mobile-debug-mcp 0.34.0 → 0.36.0
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.
- package/.agents/skills/mcp-builder/SKILL.md +44 -64
- package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
- package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
- package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
- package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
- package/.agents/skills/test-authoring/SKILL.md +7 -6
- package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
- package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
- package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.github/copilot-instructions.md +14 -36
- package/.github/workflows/ci.yml +4 -4
- package/AGENTS.md +12 -3
- package/CONTRIBUTING.md +79 -0
- package/README.md +218 -129
- package/agent-plugin/.antigravity-plugin/README.md +48 -0
- package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
- package/agent-plugin/.claude-plugin/plugin.json +2 -2
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.cursor-plugin/plugin.json +2 -2
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/README.md +50 -11
- package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
- package/agent-plugin/cursor/LICENSE +201 -0
- package/agent-plugin/cursor/README.md +9 -0
- package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
- package/agent-plugin/mcp.json +1 -1
- package/agent-plugin/mcp_config.json +13 -0
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
- package/dist/contracts/execution-controls.js +24 -0
- package/dist/contracts/selector.js +8 -0
- package/dist/contracts/target.js +15 -0
- package/dist/contracts/ui.js +1 -0
- package/dist/debugger/android.js +2 -1
- package/dist/debugger/diagnostics.js +54 -0
- package/dist/debugger/guard.js +4 -4
- package/dist/debugger/ios-worker.js +62 -0
- package/dist/debugger/ios.js +57 -0
- package/dist/debugger/protocol.js +33 -0
- package/dist/debugger/sessions.js +133 -74
- package/dist/debugger/worker.js +59 -46
- package/dist/interact/adjustment/adjustment-context.js +41 -0
- package/dist/interact/adjustment/adjustment-gestures.js +55 -0
- package/dist/interact/adjustment/adjustment-plan.js +18 -0
- package/dist/interact/adjustment/adjustment-resolution.js +127 -0
- package/dist/interact/adjustment/adjustment-result.js +19 -0
- package/dist/interact/adjustment/adjustment-semantic.js +64 -0
- package/dist/interact/adjustment/adjustment-verification.js +35 -0
- package/dist/interact/adjustment/adjustment.js +52 -0
- package/dist/interact/adjustment/control-geometry.js +87 -0
- package/dist/interact/adjustment/control-values.js +136 -0
- package/dist/interact/adjustment/slider-labels.js +84 -0
- package/dist/interact/assertions/screen-assertion.js +37 -0
- package/dist/interact/assertions/state-assertion-context.js +35 -0
- package/dist/interact/assertions/state-assertion.js +76 -0
- package/dist/interact/assertions/state-comparison.js +141 -0
- package/dist/interact/assertions/visibility-assertion.js +34 -0
- package/dist/interact/conditional-action.js +12 -7
- package/dist/interact/index.js +77 -3092
- package/dist/interact/input-service.js +1 -0
- package/dist/interact/interaction-trace.js +21 -0
- package/dist/interact/model.js +1 -0
- package/dist/interact/observation.js +1 -0
- package/dist/interact/targeting/candidate-ancestor.js +58 -0
- package/dist/interact/targeting/candidate-result.js +23 -0
- package/dist/interact/targeting/candidate-scoring.js +50 -0
- package/dist/interact/targeting/element-helpers.js +216 -0
- package/dist/interact/targeting/find-element.js +65 -0
- package/dist/interact/targeting/nearby-control.js +56 -0
- package/dist/interact/targeting/resolved-elements.js +21 -0
- package/dist/interact/targeting/tap-target.js +17 -0
- package/dist/interact/targeting/target-scope.js +104 -0
- package/dist/interact/waits/legacy-log-wait.js +38 -0
- package/dist/interact/waits/legacy-wait-observations.js +103 -0
- package/dist/interact/waits/legacy-wait-state.js +34 -0
- package/dist/interact/waits/legacy-wait.js +48 -0
- package/dist/interact/waits/screen-wait.js +49 -0
- package/dist/interact/waits/ui-change-helpers.js +157 -0
- package/dist/interact/waits/ui-change-state.js +18 -0
- package/dist/interact/waits/ui-change-wait.js +77 -0
- package/dist/interact/waits/ui-wait-condition.js +56 -0
- package/dist/interact/waits/ui-wait-state.js +38 -0
- package/dist/interact/waits/ui-wait.js +106 -0
- package/dist/manage/android.js +55 -51
- package/dist/manage/index.js +38 -75
- package/dist/manage/ios/build-types.js +1 -0
- package/dist/manage/ios/build.js +210 -0
- package/dist/manage/ios/project-discovery.js +29 -0
- package/dist/manage/ios/scheme.js +18 -0
- package/dist/manage/ios.js +44 -256
- package/dist/observe/android/context.js +1 -0
- package/dist/observe/android/current-screen.js +55 -0
- package/dist/observe/android/fingerprint.js +13 -0
- package/dist/observe/android/log-stream.js +132 -0
- package/dist/observe/android/logs.js +107 -0
- package/dist/observe/android/screenshot.js +70 -0
- package/dist/observe/android/ui-tree.js +107 -0
- package/dist/observe/android.js +16 -520
- package/dist/observe/evidence.js +114 -75
- package/dist/observe/index.js +16 -263
- package/dist/observe/ios/context.js +16 -0
- package/dist/observe/ios/fingerprint.js +12 -0
- package/dist/observe/ios/log-stream.js +105 -0
- package/dist/observe/ios/logs.js +168 -0
- package/dist/observe/ios/screenshot.js +22 -0
- package/dist/observe/ios/ui-elements.js +241 -0
- package/dist/observe/ios/ui-tree.js +137 -0
- package/dist/observe/ios-trace/artifact.js +63 -0
- package/dist/observe/ios-trace/capabilities.js +86 -0
- package/dist/observe/ios-trace/native.js +93 -0
- package/dist/observe/ios-trace/notification.js +35 -0
- package/dist/observe/ios-trace/process.js +98 -0
- package/dist/observe/ios-trace/sessions.js +244 -0
- package/dist/observe/ios.js +17 -715
- package/dist/observe/logs/model.js +1 -0
- package/dist/{network → observe/network}/index.js +2 -2
- package/dist/observe/readiness-service.js +5 -3
- package/dist/observe/readiness.js +33 -14
- package/dist/observe/screenshot-encoding.js +41 -0
- package/dist/observe/snapshot-collection.js +79 -0
- package/dist/observe/snapshot-logs.js +30 -0
- package/dist/observe/snapshot-metadata.js +43 -31
- package/dist/observe/snapshot-semantic.js +115 -0
- package/dist/observe/system-trace-error.js +7 -0
- package/dist/observe/system-trace-router.js +81 -0
- package/dist/observe/system-trace.js +26 -21
- package/dist/runtime/action-results.js +270 -0
- package/dist/runtime/clock.js +4 -0
- package/dist/server/common.js +1 -256
- package/dist/server/definitions/assert.js +162 -0
- package/dist/server/definitions/diagnostic.js +118 -0
- package/dist/server/definitions/input.js +274 -0
- package/dist/server/definitions/manage.js +194 -0
- package/dist/server/definitions/observe.js +149 -0
- package/dist/server/definitions/target.js +138 -0
- package/dist/server/definitions/wait.js +105 -0
- package/dist/server/handlers/action-execution.js +163 -0
- package/dist/server/handlers/correlation.js +23 -0
- package/dist/server/handlers/debugger-handlers.js +67 -0
- package/dist/server/handlers/debugger-response.js +9 -0
- package/dist/server/handlers/diagnostic.js +77 -0
- package/dist/server/handlers/handler-types.js +1 -0
- package/dist/server/handlers/input.js +154 -0
- package/dist/server/handlers/interact.js +113 -0
- package/dist/server/handlers/journey-assertions.js +35 -0
- package/dist/server/handlers/journey.js +147 -0
- package/dist/server/handlers/manage.js +119 -0
- package/dist/server/handlers/observe.js +95 -0
- package/dist/server/handlers/payload.js +4 -0
- package/dist/server/handlers/tool-handlers.js +170 -0
- package/dist/server/schemas/condition-schema.js +2 -0
- package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
- package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
- package/dist/server/schemas/system-trace-schema.js +22 -0
- package/dist/server/tool-definitions.js +58 -1135
- package/dist/server-core.js +2 -2
- package/dist/server.js +4 -4
- package/dist/system/android-status.js +101 -0
- package/dist/system/android.js +37 -27
- package/dist/system/index.js +13 -439
- package/dist/system/ios-status.js +132 -0
- package/dist/system/ios.js +11 -8
- package/dist/system/model.js +1 -0
- package/dist/system/probes.js +105 -0
- package/dist/system/status-summary.js +43 -0
- package/dist/system/unsupported-status.js +52 -0
- package/dist/utils/android/commands.js +89 -0
- package/dist/utils/android/gradle.js +139 -0
- package/dist/utils/android/logs.js +103 -0
- package/dist/utils/android/ui-tree.js +233 -0
- package/dist/utils/android/utils.js +6 -564
- package/dist/utils/cli/idb/idb-helper.js +4 -6
- package/dist/utils/cli/idb/install-idb.js +13 -9
- package/dist/utils/cli/ios/preflight-ios.js +42 -38
- package/dist/utils/ios/utils.js +48 -46
- package/dist/utils/java.js +65 -49
- package/dist/utils/resolve-device.js +4 -2
- package/dist/utils/ui/index.js +26 -26
- package/docs/CHANGELOG.md +67 -1
- package/docs/README.md +38 -0
- package/docs/agent-plugin-installation-verification.md +39 -0
- package/docs/benchmarks/README.md +18 -0
- package/docs/benchmarks/interaction-latency/README.md +14 -0
- package/docs/benchmarks/interaction-latency/final.md +4 -1
- package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
- package/docs/benchmarks/ui-tree-performance/README.md +24 -16
- package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
- package/docs/code-structure.md +120 -0
- package/docs/specs/001-state-verification/spec.md +16 -14
- package/docs/specs/002-richer-element-identity/spec.md +12 -10
- package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
- package/docs/specs/004-action-verification-routing/spec.md +3 -3
- package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
- package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
- package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
- package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
- package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
- package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
- package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
- package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
- package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
- package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
- package/docs/specs/014-actionability-resolution/spec.md +20 -18
- package/docs/specs/015-adjustable-control-support/spec.md +17 -15
- package/docs/specs/016-environment-tools/spec.md +20 -20
- package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
- package/docs/specs/020-conditional-actions/intent.md +5 -3
- package/docs/specs/020-conditional-actions/plan.md +8 -9
- package/docs/specs/020-conditional-actions/spec.md +4 -2
- package/docs/specs/021-app-readiness-signals/intent.md +4 -2
- package/docs/specs/021-app-readiness-signals/plan.md +7 -6
- package/docs/specs/021-app-readiness-signals/spec.md +3 -1
- package/docs/specs/022-correlated-evidence/intent.md +4 -4
- package/docs/specs/022-correlated-evidence/plan.md +10 -11
- package/docs/specs/022-correlated-evidence/spec.md +3 -1
- package/docs/specs/023-android-system-traces/intent.md +3 -2
- package/docs/specs/023-android-system-traces/plan.md +5 -7
- package/docs/specs/023-android-system-traces/spec.md +3 -1
- package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
- package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
- package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
- package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
- package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
- package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
- package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
- package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
- package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
- package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
- package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
- package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
- package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
- package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
- package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
- package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
- package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
- package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
- package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
- package/docs/specs/030-android-coroutine-validation/report.md +240 -0
- package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
- package/docs/specs/031-ios-system-traces/intent.md +38 -0
- package/docs/specs/031-ios-system-traces/investigation.md +92 -0
- package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
- package/docs/specs/031-ios-system-traces/plan.md +138 -0
- package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
- package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
- package/docs/specs/031-ios-system-traces/spec.md +135 -0
- package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
- package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
- package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
- package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
- package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
- package/docs/tools/TOOLS.md +31 -18
- package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
- package/docs/tools/debugger.md +120 -4
- package/docs/tools/interact.md +42 -20
- package/docs/tools/manage.md +25 -44
- package/docs/tools/observe.md +55 -23
- package/docs/tools/system-traces.md +89 -14
- package/docs/tools/system.md +12 -5
- package/package.json +12 -8
- package/scripts/agent-plugin.mjs +55 -5
- package/scripts/architecture-graph.mjs +69 -0
- package/scripts/check-architecture.mjs +47 -0
- package/scripts/code-quality-baseline.json +1 -0
- package/scripts/code-quality.mjs +161 -0
- package/src/contracts/execution-controls.ts +26 -0
- package/src/contracts/selector.ts +17 -0
- package/src/contracts/target.ts +19 -0
- package/src/contracts/ui.ts +94 -0
- package/src/debugger/android.ts +3 -2
- package/src/debugger/diagnostics.ts +43 -0
- package/src/debugger/guard.ts +10 -5
- package/src/debugger/ios-worker.ts +49 -0
- package/src/debugger/ios.ts +45 -0
- package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
- package/src/debugger/lldb/breakpoints.py +51 -0
- package/src/debugger/lldb/inspection.py +109 -0
- package/src/debugger/lldb/worker.py +203 -0
- package/src/debugger/protocol.ts +37 -0
- package/src/debugger/sessions.ts +326 -155
- package/src/debugger/worker.ts +57 -41
- package/src/interact/adjustment/adjustment-context.ts +83 -0
- package/src/interact/adjustment/adjustment-gestures.ts +48 -0
- package/src/interact/adjustment/adjustment-plan.ts +23 -0
- package/src/interact/adjustment/adjustment-resolution.ts +104 -0
- package/src/interact/adjustment/adjustment-result.ts +34 -0
- package/src/interact/adjustment/adjustment-semantic.ts +53 -0
- package/src/interact/adjustment/adjustment-verification.ts +60 -0
- package/src/interact/adjustment/adjustment.ts +49 -0
- package/src/interact/adjustment/control-geometry.ts +120 -0
- package/src/interact/adjustment/control-values.ts +141 -0
- package/src/interact/adjustment/slider-labels.ts +84 -0
- package/src/interact/assertions/screen-assertion.ts +39 -0
- package/src/interact/assertions/state-assertion-context.ts +48 -0
- package/src/interact/assertions/state-assertion.ts +64 -0
- package/src/interact/assertions/state-comparison.ts +168 -0
- package/src/interact/assertions/visibility-assertion.ts +41 -0
- package/src/interact/conditional-action.ts +18 -12
- package/src/interact/index.ts +89 -3494
- package/src/interact/input-service.ts +11 -0
- package/src/interact/interaction-trace.ts +36 -0
- package/src/interact/model.ts +109 -0
- package/src/interact/observation.ts +7 -0
- package/src/interact/targeting/candidate-ancestor.ts +49 -0
- package/src/interact/targeting/candidate-result.ts +24 -0
- package/src/interact/targeting/candidate-scoring.ts +42 -0
- package/src/interact/targeting/element-helpers.ts +243 -0
- package/src/interact/targeting/find-element.ts +47 -0
- package/src/interact/targeting/nearby-control.ts +46 -0
- package/src/interact/targeting/resolved-elements.ts +20 -0
- package/src/interact/targeting/tap-target.ts +22 -0
- package/src/interact/targeting/target-scope.ts +121 -0
- package/src/interact/waits/legacy-log-wait.ts +30 -0
- package/src/interact/waits/legacy-wait-observations.ts +55 -0
- package/src/interact/waits/legacy-wait-state.ts +48 -0
- package/src/interact/waits/legacy-wait.ts +34 -0
- package/src/interact/waits/screen-wait.ts +37 -0
- package/src/interact/waits/ui-change-helpers.ts +184 -0
- package/src/interact/waits/ui-change-state.ts +26 -0
- package/src/interact/waits/ui-change-wait.ts +59 -0
- package/src/interact/waits/ui-wait-condition.ts +52 -0
- package/src/interact/waits/ui-wait-state.ts +55 -0
- package/src/interact/waits/ui-wait.ts +84 -0
- package/src/manage/android.ts +56 -50
- package/src/manage/index.ts +63 -93
- package/src/manage/ios/build-types.ts +14 -0
- package/src/manage/ios/build.ts +230 -0
- package/src/manage/ios/project-discovery.ts +26 -0
- package/src/manage/ios/scheme.ts +16 -0
- package/src/manage/ios.ts +67 -298
- package/src/observe/android/context.ts +1 -0
- package/src/observe/android/current-screen.ts +63 -0
- package/src/observe/android/fingerprint.ts +13 -0
- package/src/observe/android/log-stream.ts +127 -0
- package/src/observe/android/logs.ts +110 -0
- package/src/observe/android/screenshot.ts +79 -0
- package/src/observe/android/ui-tree.ts +113 -0
- package/src/observe/android.ts +17 -531
- package/src/observe/evidence.ts +122 -70
- package/src/observe/index.ts +24 -286
- package/src/observe/ios/context.ts +22 -0
- package/src/observe/ios/fingerprint.ts +12 -0
- package/src/observe/ios/log-stream.ts +103 -0
- package/src/observe/ios/logs.ts +179 -0
- package/src/observe/ios/screenshot.ts +26 -0
- package/src/observe/ios/ui-elements.ts +266 -0
- package/src/observe/ios/ui-tree.ts +143 -0
- package/src/observe/ios-trace/artifact.ts +55 -0
- package/src/observe/ios-trace/capabilities.ts +74 -0
- package/src/observe/ios-trace/native.ts +72 -0
- package/src/observe/ios-trace/notification.ts +30 -0
- package/src/observe/ios-trace/process.ts +82 -0
- package/src/observe/ios-trace/sessions.ts +204 -0
- package/src/observe/ios.ts +18 -749
- package/src/observe/logs/model.ts +11 -0
- package/src/{network → observe/network}/index.ts +2 -2
- package/src/observe/readiness-service.ts +6 -4
- package/src/observe/readiness.ts +42 -21
- package/src/observe/screenshot-encoding.ts +32 -0
- package/src/observe/snapshot-collection.ts +69 -0
- package/src/observe/snapshot-logs.ts +26 -0
- package/src/observe/snapshot-metadata.ts +51 -31
- package/src/observe/snapshot-semantic.ts +153 -0
- package/src/observe/system-trace-error.ts +3 -0
- package/src/observe/system-trace-router.ts +61 -0
- package/src/observe/system-trace.ts +28 -19
- package/src/runtime/action-results.ts +376 -0
- package/src/runtime/clock.ts +9 -0
- package/src/server/common.ts +9 -359
- package/src/server/definitions/assert.ts +164 -0
- package/src/server/definitions/diagnostic.ts +120 -0
- package/src/server/definitions/input.ts +276 -0
- package/src/server/definitions/manage.ts +196 -0
- package/src/server/definitions/observe.ts +151 -0
- package/src/server/definitions/target.ts +140 -0
- package/src/server/definitions/wait.ts +107 -0
- package/src/server/handlers/action-execution.ts +164 -0
- package/src/server/handlers/correlation.ts +20 -0
- package/src/server/handlers/debugger-handlers.ts +58 -0
- package/src/server/handlers/debugger-response.ts +7 -0
- package/src/server/handlers/diagnostic.ts +86 -0
- package/src/server/handlers/handler-types.ts +7 -0
- package/src/server/handlers/input.ts +173 -0
- package/src/server/handlers/interact.ts +135 -0
- package/src/server/handlers/journey-assertions.ts +39 -0
- package/src/server/handlers/journey.ts +132 -0
- package/src/server/handlers/manage.ts +141 -0
- package/src/server/handlers/observe.ts +116 -0
- package/src/server/handlers/payload.ts +36 -0
- package/src/server/handlers/tool-handlers.ts +170 -0
- package/src/server/schemas/condition-schema.ts +2 -0
- package/src/server/schemas/debugger-schema.ts +46 -0
- package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
- package/src/server/schemas/system-trace-schema.ts +23 -0
- package/src/server/tool-definitions.ts +67 -1143
- package/src/server-core.ts +2 -2
- package/src/server.ts +4 -4
- package/src/system/android-status.ts +124 -0
- package/src/system/android.ts +33 -21
- package/src/system/index.ts +21 -525
- package/src/system/ios-status.ts +147 -0
- package/src/system/ios.ts +14 -10
- package/src/system/model.ts +41 -0
- package/src/system/probes.ts +114 -0
- package/src/system/status-summary.ts +47 -0
- package/src/system/unsupported-status.ts +55 -0
- package/src/types.ts +16 -106
- package/src/utils/android/commands.ts +81 -0
- package/src/utils/android/gradle.ts +132 -0
- package/src/utils/android/logs.ts +103 -0
- package/src/utils/android/ui-tree.ts +239 -0
- package/src/utils/android/utils.ts +16 -556
- package/src/utils/cli/idb/check-idb.ts +4 -4
- package/src/utils/cli/idb/idb-helper.ts +6 -8
- package/src/utils/cli/idb/install-idb.ts +15 -9
- package/src/utils/cli/ios/preflight-ios.ts +39 -31
- package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
- package/src/utils/ios/utils.ts +60 -61
- package/src/utils/java.ts +62 -43
- package/src/utils/resolve-device.ts +9 -6
- package/src/utils/ui/index.ts +28 -28
- package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
- package/test/device/automated/debugger/coroutine-support.ts +140 -0
- package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
- package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
- package/test/device/automated/debugger/runtime.android.test.ts +19 -3
- package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
- package/test/device/automated/interact/interaction.android.test.ts +56 -0
- package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
- package/test/device/automated/observe/system-trace.smoke.ts +1 -1
- package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
- package/test/fixtures/contracts/tool-definitions.json +3696 -0
- package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
- package/test/fixtures/coroutine-app/README.md +48 -0
- package/test/fixtures/coroutine-app/build.ts +35 -0
- package/test/fixtures/debugger-app/MainActivity.java +21 -0
- package/test/fixtures/debugger-app/MainActivity.kt +32 -0
- package/test/fixtures/debugger-app/README.md +52 -0
- package/test/fixtures/debugger-app/build.ts +25 -15
- package/test/fixtures/ios-debugger-app/Main.swift +95 -0
- package/test/fixtures/ios-debugger-app/README.md +27 -0
- package/test/fixtures/ios-debugger-app/build.ts +21 -0
- package/test/fixtures/latency-journey-app/README.md +2 -0
- package/test/fixtures/readiness-app/README.md +2 -0
- package/test/helpers/ios-trace-shutdown.ts +29 -0
- package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
- package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
- package/test/unit/debugger/diagnostics.test.ts +80 -0
- package/test/unit/debugger/guards.test.ts +4 -0
- package/test/unit/debugger/handle-guards.test.ts +88 -0
- package/test/unit/debugger/ios-preparation.test.ts +57 -0
- package/test/unit/debugger/ios.test.ts +104 -0
- package/test/unit/debugger/lldb-worker.py +166 -0
- package/test/unit/debugger/lldb-worker.test.ts +10 -0
- package/test/unit/debugger/preparation.test.ts +32 -0
- package/test/unit/debugger/protocol.test.ts +15 -0
- package/test/unit/debugger/sessions.test.ts +1 -1
- package/test/unit/interact/adjustment_service.test.ts +42 -0
- package/test/unit/interact/adjustment_verification.test.ts +35 -0
- package/test/unit/interact/clock_dependencies.test.ts +63 -0
- package/test/unit/interact/element_helpers.test.ts +19 -0
- package/test/unit/interact/legacy_wait_service.test.ts +17 -0
- package/test/unit/interact/state_comparison.test.ts +20 -0
- package/test/unit/interact/target_scope.test.ts +35 -0
- package/test/unit/network/get_network_activity.test.ts +1 -1
- package/test/unit/observe/ios-trace-native.test.ts +90 -0
- package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
- package/test/unit/observe/readiness.test.ts +1 -1
- package/test/unit/observe/screenshot-encoding.test.ts +30 -0
- package/test/unit/observe/snapshot-logs.test.ts +14 -0
- package/test/unit/observe/system-trace-router.test.ts +49 -0
- package/test/unit/quality/agent-plugin-export.test.ts +38 -0
- package/test/unit/quality/architecture.test.ts +60 -0
- package/test/unit/quality/code-quality.test.ts +58 -0
- package/test/unit/quality/interaction_boundaries.test.ts +24 -0
- package/test/unit/server/conditional_actions.test.ts +1 -1
- package/test/unit/server/conditional_schema.test.ts +2 -2
- package/test/unit/server/debugger-response.test.ts +63 -0
- package/test/unit/server/system-trace.test.ts +12 -3
- package/test/unit/server/tool_definitions_parity.test.ts +8 -0
- package/dist/server/debugger-handlers.js +0 -38
- package/dist/server/system-trace-schema.js +0 -17
- package/dist/server/tool-handlers.js +0 -1000
- package/docs/ROADMAP.md +0 -646
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
- package/docs/specs/baseline-spec-v0.md +0 -312
- package/docs/specs/mcp-tooling-spec-v1.md +0 -446
- package/src/server/debugger-handlers.ts +0 -29
- package/src/server/debugger-schema.ts +0 -37
- package/src/server/system-trace-schema.ts +0 -17
- package/src/server/tool-handlers.ts +0 -1008
- /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
- /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
- /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
- /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { systemClock, type Clock } from '../../runtime/clock.js'
|
|
2
|
+
import type { ActionTargetResolved } from '../../types.js'
|
|
3
|
+
import type { UiSelector } from '../../contracts/selector.js'
|
|
4
|
+
import type { UiTree, ObserveUI } from '../observation.js'
|
|
5
|
+
import type { UiElement } from '../model.js'
|
|
6
|
+
import type { UICondition } from './ui-wait-condition.js'
|
|
7
|
+
import { currentReadinessWait } from '../../observe/readiness.js'
|
|
8
|
+
|
|
9
|
+
export interface UIWaitInput {
|
|
10
|
+
selector?: UiSelector; condition?: UICondition; timeout_ms?: number; poll_interval_ms?: number
|
|
11
|
+
match?: { index?: number }; retry?: { max_attempts?: number; backoff_ms?: number }
|
|
12
|
+
platform?: 'android' | 'ios'; deviceId?: string; singleObservation?: boolean; singleAttempt?: boolean
|
|
13
|
+
rejectAmbiguous?: boolean; resolveTapTarget?: boolean
|
|
14
|
+
}
|
|
15
|
+
export interface UIWaitDependencies {
|
|
16
|
+
clock?: Clock
|
|
17
|
+
observe: ObserveUI
|
|
18
|
+
resolve(platform: 'android' | 'ios', deviceId: string | undefined, element: UiElement, index: number): ActionTargetResolved
|
|
19
|
+
}
|
|
20
|
+
export interface UIWaitResult {
|
|
21
|
+
status: string; error?: { code: string; message?: string }; matched?: number; element?: ActionTargetResolved | null; _tree?: UiTree
|
|
22
|
+
metrics?: { latency_ms: number; poll_count: number; attempts: number }
|
|
23
|
+
requested?: { selector: UiSelector; condition: UICondition; match: { index: number } | null }
|
|
24
|
+
observed?: { matched_count: number; condition_satisfied: boolean; selected_index: number | null; last_matched_element: ActionTargetResolved | null }
|
|
25
|
+
}
|
|
26
|
+
export function createUIWait(input: UIWaitInput, clock: Clock = systemClock) {
|
|
27
|
+
const tracker = currentReadinessWait(), start = clock.now()
|
|
28
|
+
const requestedIndex = typeof input.match?.index === 'number' ? input.match.index : null
|
|
29
|
+
const args = { condition: 'exists' as UICondition, timeout_ms: 60000, poll_interval_ms: 300,
|
|
30
|
+
retry: { max_attempts: 1, backoff_ms: 0 }, singleObservation: false, singleAttempt: false,
|
|
31
|
+
rejectAmbiguous: false, resolveTapTarget: false, ...input }
|
|
32
|
+
// Explicit undefined values have the same defaults as destructured handler parameters.
|
|
33
|
+
if (args.condition === undefined) args.condition = 'exists'
|
|
34
|
+
if (args.timeout_ms === undefined) args.timeout_ms = 60000
|
|
35
|
+
if (args.poll_interval_ms === undefined) args.poll_interval_ms = 300
|
|
36
|
+
if (args.retry === undefined) args.retry = { max_attempts: 1, backoff_ms: 0 }
|
|
37
|
+
const requested = { selector: args.selector ?? {}, condition: args.condition, match: requestedIndex === null ? null : { index: requestedIndex } }
|
|
38
|
+
return { clock, args, requestedIndex, requested, tracker, start, attempts: 0, polls: 0,
|
|
39
|
+
count: 0, element: null as ActionTargetResolved | null, met: false, stable: 0 }
|
|
40
|
+
}
|
|
41
|
+
export type UIWaitState = ReturnType<typeof createUIWait>
|
|
42
|
+
export function waitObserved(s: UIWaitState, condition = s.met) {
|
|
43
|
+
return { matched_count: s.count, condition_satisfied: condition, selected_index: s.element?.index ?? null, last_matched_element: s.element }
|
|
44
|
+
}
|
|
45
|
+
export function waitFailure(s: UIWaitState, code: string, message: string, observed = waitObserved(s, false)): UIWaitResult {
|
|
46
|
+
return { status: 'timeout', error: { code, message }, metrics: { latency_ms: s.clock.now() - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed }
|
|
47
|
+
}
|
|
48
|
+
export function validateUIWait(s: UIWaitState) {
|
|
49
|
+
const { selector, condition, platform } = s.args
|
|
50
|
+
const valid = [selector?.text, selector?.resource_id, selector?.accessibility_id].some(value => typeof value === 'string' && value.trim().length > 0)
|
|
51
|
+
if (!valid) return waitFailure(s, 'INVALID_SELECTOR', 'Selector must include at least one non-empty field: text, resource_id, or accessibility_id')
|
|
52
|
+
if (!['exists', 'not_exists', 'visible', 'clickable'].includes(condition)) return waitFailure(s, 'INVALID_CONDITION', `Unsupported condition: ${condition}`)
|
|
53
|
+
if (platform && !['android', 'ios'].includes(platform)) return waitFailure(s, 'PLATFORM_NOT_SUPPORTED', `Unsupported platform: ${platform}`)
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { systemClock } from '../../runtime/clock.js'
|
|
2
|
+
import type { UiTree } from '../observation.js'
|
|
3
|
+
import { remainingBudget } from '../../utils/operation-budget.js'
|
|
4
|
+
import { checkDebuggerGuard } from '../../debugger/guard.js'
|
|
5
|
+
import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js'
|
|
6
|
+
import { readinessDelay } from '../../observe/readiness-service.js'
|
|
7
|
+
import { matchingElements, evaluateCondition } from './ui-wait-condition.js'
|
|
8
|
+
import { createUIWait, validateUIWait, waitObserved, waitFailure,
|
|
9
|
+
type UIWaitInput, type UIWaitDependencies, type UIWaitState, type UIWaitResult } from './ui-wait-state.js'
|
|
10
|
+
|
|
11
|
+
async function delay(s: UIWaitState, ms: number) {
|
|
12
|
+
await (s.tracker && s.clock === systemClock ? readinessDelay(ms) : s.clock.sleep(ms))
|
|
13
|
+
}
|
|
14
|
+
function acceptObservation(s: UIWaitState, tree: UiTree, now: number): UIWaitResult | null {
|
|
15
|
+
const age = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null
|
|
16
|
+
if (!(s.met && (age === null || age <= 500))) { s.stable = 0; return null }
|
|
17
|
+
s.stable++
|
|
18
|
+
if (s.stable < (s.args.singleObservation ? 1 : 2) || (s.tracker && s.tracker.summary.state !== 'settled')) return null
|
|
19
|
+
return { status: 'success', ...(s.args.singleObservation ? { _tree: tree } : {}), matched: s.count,
|
|
20
|
+
element: s.element, metrics: { latency_ms: now - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed: waitObserved(s, true) }
|
|
21
|
+
}
|
|
22
|
+
function inspectObservation(s: UIWaitState, tree: UiTree, deps: UIWaitDependencies): UIWaitResult | null {
|
|
23
|
+
const a = s.args, elements = tree && Array.isArray(tree.elements) ? tree.elements : []
|
|
24
|
+
const matches = matchingElements(elements, a.selector!)
|
|
25
|
+
if (a.rejectAmbiguous && matches.length > 1 && a.match?.index === undefined) return { status: 'timeout', error: { code: 'AMBIGUOUS_TARGET' } }
|
|
26
|
+
const index = typeof a.match?.index === 'number' ? a.match.index : undefined
|
|
27
|
+
const condition = evaluateCondition(elements, matches, a.condition, index, a.resolveTapTarget)
|
|
28
|
+
const platform = tree?.device?.platform === 'ios' ? 'ios' : a.platform || 'android'
|
|
29
|
+
const deviceId = tree?.device?.id || a.deviceId
|
|
30
|
+
s.count = matches.length; s.met = condition.met
|
|
31
|
+
s.element = condition.element ? deps.resolve(platform, deviceId, condition.element.el, condition.element.idx) : null
|
|
32
|
+
return acceptObservation(s, tree, s.clock.now())
|
|
33
|
+
}
|
|
34
|
+
function pollError(s: UIWaitState, error: unknown) {
|
|
35
|
+
checkDebuggerGuard()
|
|
36
|
+
if (error instanceof Error && error.message === 'ACTION_TIMEOUT') throw error
|
|
37
|
+
if (s.tracker && !String(error).includes('READINESS_')) s.tracker.unavailable()
|
|
38
|
+
if (s.tracker && isDeviceUnavailable(error)) throw error
|
|
39
|
+
if (!s.tracker) console.warn('waitForUI: poll error (non-fatal):', error instanceof Error ? error.message : String(error))
|
|
40
|
+
}
|
|
41
|
+
async function poll(s: UIWaitState, deps: UIWaitDependencies): Promise<UIWaitResult | null> {
|
|
42
|
+
remainingBudget(); s.polls++
|
|
43
|
+
try {
|
|
44
|
+
const started = s.clock.now(), a = s.args
|
|
45
|
+
const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId })
|
|
46
|
+
observeReadinessWait(tree, started)
|
|
47
|
+
if (a.resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error))) throw new Error('ACTION_TIMEOUT')
|
|
48
|
+
return inspectObservation(s, tree, deps)
|
|
49
|
+
} catch (error) { pollError(s, error) }
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
async function runAttempts(s: UIWaitState, deps: UIWaitDependencies): Promise<UIWaitResult | undefined> {
|
|
53
|
+
const a = s.args
|
|
54
|
+
const interval = Math.max(50, Math.min(a.poll_interval_ms || 300, 2000))
|
|
55
|
+
const maximum = a.retry && a.retry.max_attempts ? Math.max(1, a.retry.max_attempts) : 1
|
|
56
|
+
const backoff = a.retry && a.retry.backoff_ms ? Math.max(0, a.retry.backoff_ms) : 0
|
|
57
|
+
while (s.attempts < maximum) {
|
|
58
|
+
s.attempts++
|
|
59
|
+
const deadline = s.clock.now() + (a.timeout_ms || 0)
|
|
60
|
+
while (s.clock.now() <= deadline || (a.singleAttempt && s.polls === 0)) {
|
|
61
|
+
const result = await poll(s, deps)
|
|
62
|
+
if (result) return result
|
|
63
|
+
if (a.singleAttempt) break
|
|
64
|
+
await delay(s, interval || 50)
|
|
65
|
+
}
|
|
66
|
+
if (s.attempts < maximum) { if (backoff > 0) await delay(s, backoff); continue }
|
|
67
|
+
const note = s.requestedIndex !== null && s.count <= s.requestedIndex
|
|
68
|
+
? ` requested match.index=${s.requestedIndex} but observed ${s.count} match(es)` : ` observed ${s.count} match(es)`
|
|
69
|
+
return waitFailure(s, s.tracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', `Condition ${a.condition} not satisfied within timeout;${note}`, waitObserved(s))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function waitForUI(input: UIWaitInput, deps: UIWaitDependencies) {
|
|
73
|
+
const state = createUIWait(input, deps.clock)
|
|
74
|
+
const invalid = validateUIWait(state)
|
|
75
|
+
if (invalid) return invalid
|
|
76
|
+
try { return await runAttempts(state, deps) }
|
|
77
|
+
catch (error) {
|
|
78
|
+
checkDebuggerGuard()
|
|
79
|
+
if (!state.tracker && error instanceof Error && error.message === 'ACTION_TIMEOUT') throw error
|
|
80
|
+
if (state.tracker && isDeviceUnavailable(error)) throw error
|
|
81
|
+
return waitFailure(state, state.tracker && String(error).includes('ACTION_TIMEOUT') ? 'TIMEOUT' : 'INTERNAL_ERROR',
|
|
82
|
+
state.tracker ? 'Readiness predicate wait did not complete' : error instanceof Error ? error.message : String(error), waitObserved(state, state.tracker ? state.met : false))
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/manage/android.ts
CHANGED
|
@@ -41,7 +41,7 @@ export class AndroidManage {
|
|
|
41
41
|
})
|
|
42
42
|
proc.on('error', err => reject(err))
|
|
43
43
|
})
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
const apk = await findApk(projectPath)
|
|
46
46
|
if (!apk) return { error: 'Could not find APK after build' }
|
|
47
47
|
return { artifactPath: apk }
|
|
@@ -56,54 +56,7 @@ export class AndroidManage {
|
|
|
56
56
|
|
|
57
57
|
let apkToInstall: string = apkPath
|
|
58
58
|
try {
|
|
59
|
-
|
|
60
|
-
if (stat && stat.isDirectory()) {
|
|
61
|
-
const detectedJavaHome = await detectJavaHome().catch(() => undefined)
|
|
62
|
-
const env = { ...process.env }
|
|
63
|
-
if (detectedJavaHome) {
|
|
64
|
-
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
65
|
-
env.JAVA_HOME = detectedJavaHome
|
|
66
|
-
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
|
|
67
|
-
console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
try { delete env.SHELL } catch {}
|
|
71
|
-
|
|
72
|
-
const gradleArgs = ['assembleDebug']
|
|
73
|
-
if (detectedJavaHome) {
|
|
74
|
-
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
|
|
75
|
-
gradleArgs.push('--no-daemon')
|
|
76
|
-
env.GRADLE_JAVA_HOME = detectedJavaHome
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const wrapperPath = path.join(apkPath, 'gradlew')
|
|
80
|
-
const useWrapper = existsSync(wrapperPath)
|
|
81
|
-
const execCmd = useWrapper ? wrapperPath : 'gradle'
|
|
82
|
-
const spawnOpts: any = { cwd: apkPath, env }
|
|
83
|
-
if (useWrapper) {
|
|
84
|
-
await fs.chmod(wrapperPath, 0o755).catch(() => {})
|
|
85
|
-
// Run wrapper directly to avoid shell splitting of args
|
|
86
|
-
spawnOpts.shell = false
|
|
87
|
-
} else {
|
|
88
|
-
// Execute gradle directly without a shell so paths with spaces are preserved
|
|
89
|
-
spawnOpts.shell = false
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const proc = spawn(execCmd, gradleArgs, spawnOpts)
|
|
93
|
-
let stderr = ''
|
|
94
|
-
await new Promise<void>((resolve, reject) => {
|
|
95
|
-
proc.stderr?.on('data', d => stderr += d.toString())
|
|
96
|
-
proc.on('close', code => {
|
|
97
|
-
if (code === 0) resolve()
|
|
98
|
-
else reject(new Error(stderr || `Gradle build failed with code ${code}`))
|
|
99
|
-
})
|
|
100
|
-
proc.on('error', err => reject(err))
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
const built = await findApk(apkPath)
|
|
104
|
-
if (!built) throw new Error('Could not locate built APK after running Gradle')
|
|
105
|
-
apkToInstall = built
|
|
106
|
-
}
|
|
59
|
+
apkToInstall = await prepareInstallApk(apkPath)
|
|
107
60
|
|
|
108
61
|
try {
|
|
109
62
|
const res = await spawnAdb(['install', '-r', apkToInstall], deviceId)
|
|
@@ -138,7 +91,7 @@ export class AndroidManage {
|
|
|
138
91
|
}
|
|
139
92
|
throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed')
|
|
140
93
|
} finally {
|
|
141
|
-
try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch {}
|
|
94
|
+
try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch { }
|
|
142
95
|
}
|
|
143
96
|
} catch (e) {
|
|
144
97
|
// gather diagnostics for attempted adb operations
|
|
@@ -224,3 +177,56 @@ export class AndroidManage {
|
|
|
224
177
|
}
|
|
225
178
|
}
|
|
226
179
|
}
|
|
180
|
+
|
|
181
|
+
async function prepareInstallApk(apkPath: string): Promise<string> {
|
|
182
|
+
const stat = await fs.stat(apkPath).catch(() => null)
|
|
183
|
+
if (stat && stat.isDirectory()) {
|
|
184
|
+
const detectedJavaHome = await detectJavaHome().catch(() => undefined)
|
|
185
|
+
const env = { ...process.env }
|
|
186
|
+
if (detectedJavaHome) {
|
|
187
|
+
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
188
|
+
env.JAVA_HOME = detectedJavaHome
|
|
189
|
+
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
|
|
190
|
+
console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
try { delete env.SHELL } catch { }
|
|
194
|
+
|
|
195
|
+
const gradleArgs = ['assembleDebug']
|
|
196
|
+
if (detectedJavaHome) {
|
|
197
|
+
gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
|
|
198
|
+
gradleArgs.push('--no-daemon')
|
|
199
|
+
env.GRADLE_JAVA_HOME = detectedJavaHome
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const wrapperPath = path.join(apkPath, 'gradlew')
|
|
203
|
+
const useWrapper = existsSync(wrapperPath)
|
|
204
|
+
const execCmd = useWrapper ? wrapperPath : 'gradle'
|
|
205
|
+
const spawnOpts: import('node:child_process').SpawnOptions = { cwd: apkPath, env }
|
|
206
|
+
if (useWrapper) {
|
|
207
|
+
await fs.chmod(wrapperPath, 0o755).catch(() => { })
|
|
208
|
+
// Run wrapper directly to avoid shell splitting of args
|
|
209
|
+
spawnOpts.shell = false
|
|
210
|
+
} else {
|
|
211
|
+
// Execute gradle directly without a shell so paths with spaces are preserved
|
|
212
|
+
spawnOpts.shell = false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const proc = spawn(execCmd, gradleArgs, spawnOpts)
|
|
216
|
+
let stderr = ''
|
|
217
|
+
await new Promise<void>((resolve, reject) => {
|
|
218
|
+
proc.stderr?.on('data', d => stderr += d.toString())
|
|
219
|
+
proc.on('close', code => {
|
|
220
|
+
if (code === 0) resolve()
|
|
221
|
+
else reject(new Error(stderr || `Gradle build failed with code ${code}`))
|
|
222
|
+
})
|
|
223
|
+
proc.on('error', err => reject(err))
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const built = await findApk(apkPath)
|
|
227
|
+
if (!built) throw new Error('Could not locate built APK after running Gradle')
|
|
228
|
+
return built
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return apkPath
|
|
232
|
+
}
|
package/src/manage/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IOSBuildOptions } from './ios.js'
|
|
1
2
|
import { promises as fs } from 'fs'
|
|
2
3
|
import path from 'path'
|
|
3
4
|
import { resolveTargetDevice, listDevices } from '../utils/resolve-device.js'
|
|
@@ -11,9 +12,9 @@ import type { InstallAppResponse, StartAppResponse, TerminateAppResponse, Restar
|
|
|
11
12
|
export { AndroidManage } from './android.js';
|
|
12
13
|
export { iOSManage } from './ios.js';
|
|
13
14
|
|
|
14
|
-
export async function detectProjectPlatform(projectPath: string): Promise<'ios'|'android'|'ambiguous'|'unknown'> {
|
|
15
|
+
export async function detectProjectPlatform(projectPath: string): Promise<'ios' | 'android' | 'ambiguous' | 'unknown'> {
|
|
15
16
|
// Recursively scan up to a limited depth for platform markers to avoid mis-detection
|
|
16
|
-
async function scan(dir: string, depth = 3): Promise<{ ios: boolean, android: boolean }>{
|
|
17
|
+
async function scan(dir: string, depth = 3): Promise<{ ios: boolean, android: boolean }> {
|
|
17
18
|
const res = { ios: false, android: false }
|
|
18
19
|
try {
|
|
19
20
|
const ents = await fs.readdir(dir).catch(() => [])
|
|
@@ -28,15 +29,14 @@ export async function detectProjectPlatform(projectPath: string): Promise<'ios'|
|
|
|
28
29
|
try {
|
|
29
30
|
const full = path.join(dir, e)
|
|
30
31
|
const st = await fs.stat(full).catch(() => null)
|
|
31
|
-
if (st
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} catch {}
|
|
32
|
+
if (!st || !st.isDirectory()) continue
|
|
33
|
+
const child = await scan(full, depth - 1)
|
|
34
|
+
if (child.ios) res.ios = true
|
|
35
|
+
if (child.android) res.android = true
|
|
36
|
+
if (res.ios && res.android) return res
|
|
37
|
+
} catch { }
|
|
38
38
|
}
|
|
39
|
-
} catch {}
|
|
39
|
+
} catch { }
|
|
40
40
|
return res
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -78,7 +78,7 @@ export class ToolsManage {
|
|
|
78
78
|
static async build_android({ projectPath, gradleTask, maxWorkers, gradleCache, forceClean }: { projectPath: string, gradleTask?: string, maxWorkers?: number, gradleCache?: boolean, forceClean?: boolean }) {
|
|
79
79
|
const android = new AndroidManage()
|
|
80
80
|
const task = gradleTask || 'assembleDebug'
|
|
81
|
-
return await
|
|
81
|
+
return await android.build(projectPath, {
|
|
82
82
|
variant: task,
|
|
83
83
|
env: mergeDefinedEnv({
|
|
84
84
|
MCP_GRADLE_TASK: task,
|
|
@@ -92,7 +92,7 @@ export class ToolsManage {
|
|
|
92
92
|
static async build_ios({ projectPath, workspace: _workspace, project: _project, scheme: _scheme, destinationUDID, derivedDataPath, buildJobs, forceClean }: { projectPath: string, workspace?: string, project?: string, scheme?: string, destinationUDID?: string, derivedDataPath?: string, buildJobs?: number, forceClean?: boolean }) {
|
|
93
93
|
const ios = new iOSManage()
|
|
94
94
|
|
|
95
|
-
const opts:
|
|
95
|
+
const opts: IOSBuildOptions = {}
|
|
96
96
|
if (_workspace) opts.workspace = _workspace
|
|
97
97
|
if (_project) opts.project = _project
|
|
98
98
|
if (_scheme) opts.scheme = _scheme
|
|
@@ -103,7 +103,7 @@ export class ToolsManage {
|
|
|
103
103
|
// prefer explicit xcodebuild path from env
|
|
104
104
|
if (process.env.XCODEBUILD_PATH) opts.xcodeCmd = process.env.XCODEBUILD_PATH
|
|
105
105
|
|
|
106
|
-
return await
|
|
106
|
+
return await ios.build(projectPath, {
|
|
107
107
|
...opts,
|
|
108
108
|
env: mergeDefinedEnv({
|
|
109
109
|
MCP_DERIVED_DATA: derivedDataPath,
|
|
@@ -115,7 +115,7 @@ export class ToolsManage {
|
|
|
115
115
|
})
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android'|'ios', buildMode?: 'debug'|'release'|'profile', maxWorkers?: number, forceClean?: boolean }) {
|
|
118
|
+
static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android' | 'ios', buildMode?: 'debug' | 'release' | 'profile', maxWorkers?: number, forceClean?: boolean }) {
|
|
119
119
|
// Prefer using flutter CLI when available; otherwise delegate to native subproject builders
|
|
120
120
|
const flutterCmd = process.env.FLUTTER_PATH || 'flutter'
|
|
121
121
|
// silence unused params
|
|
@@ -131,10 +131,8 @@ export class ToolsManage {
|
|
|
131
131
|
// Try to find built APK
|
|
132
132
|
const apk = await findApk(path.join(projectPath))
|
|
133
133
|
if (apk) return { artifactPath: apk, output: out }
|
|
134
|
-
} catch (
|
|
135
|
-
|
|
136
|
-
const stderr = err && err.stderr ? String(err.stderr) : ''
|
|
137
|
-
throw new Error(`flutter build apk failed: ${stderr || stdout || err.message}`)
|
|
134
|
+
} catch (thrown: unknown) {
|
|
135
|
+
throw flutterBuildError(thrown, 'apk')
|
|
138
136
|
}
|
|
139
137
|
}
|
|
140
138
|
|
|
@@ -144,10 +142,8 @@ export class ToolsManage {
|
|
|
144
142
|
const out = execSync(`${flutterCmd} build ios --${mode} --no-codesign`, { cwd: projectPath, encoding: 'utf8' })
|
|
145
143
|
const app = await findAppBundle(path.join(projectPath))
|
|
146
144
|
if (app) return { artifactPath: app, output: out }
|
|
147
|
-
} catch (
|
|
148
|
-
|
|
149
|
-
const stderr = err && err.stderr ? String(err.stderr) : ''
|
|
150
|
-
throw new Error(`flutter build ios failed: ${stderr || stdout || err.message}`)
|
|
145
|
+
} catch (thrown: unknown) {
|
|
146
|
+
throw flutterBuildError(thrown, 'ios')
|
|
151
147
|
}
|
|
152
148
|
}
|
|
153
149
|
} catch (e) {
|
|
@@ -156,38 +152,24 @@ export class ToolsManage {
|
|
|
156
152
|
void e
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
if (!platform || platform === 'android') {
|
|
161
|
-
const androidDir = path.join(projectPath, 'android')
|
|
162
|
-
const android = new AndroidManage()
|
|
163
|
-
const artifact = await (android as any).build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug')
|
|
164
|
-
return artifact
|
|
165
|
-
}
|
|
166
|
-
if (!platform || platform === 'ios') {
|
|
167
|
-
const iosDir = path.join(projectPath, 'ios')
|
|
168
|
-
const ios = new iOSManage()
|
|
169
|
-
const artifact = await (ios as any).build(iosDir)
|
|
170
|
-
return artifact
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return { error: 'Unable to build flutter project' }
|
|
155
|
+
return buildFlutterNative(projectPath, platform, _forceClean)
|
|
174
156
|
}
|
|
175
157
|
|
|
176
|
-
static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android'|'ios', variant?: string, maxWorkers?: number, forceClean?: boolean }) {
|
|
158
|
+
static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android' | 'ios', variant?: string, maxWorkers?: number, forceClean?: boolean }) {
|
|
177
159
|
// silence unused params
|
|
178
160
|
void _maxWorkers; void _forceClean;
|
|
179
161
|
// React Native typically uses native subprojects. Delegate to Android/iOS builders.
|
|
180
162
|
if (!platform || platform === 'android') {
|
|
181
163
|
const androidDir = path.join(projectPath, 'android')
|
|
182
164
|
const android = new AndroidManage()
|
|
183
|
-
const artifact = await
|
|
165
|
+
const artifact = await android.build(androidDir, variant || 'assembleDebug')
|
|
184
166
|
return artifact
|
|
185
167
|
}
|
|
186
168
|
if (!platform || platform === 'ios') {
|
|
187
169
|
const iosDir = path.join(projectPath, 'ios')
|
|
188
170
|
// Recommend running `pod install` prior to building in CI; not performed automatically here
|
|
189
171
|
const ios = new iOSManage()
|
|
190
|
-
const artifact = await
|
|
172
|
+
const artifact = await ios.build(iosDir)
|
|
191
173
|
return artifact
|
|
192
174
|
}
|
|
193
175
|
return { error: 'Unable to build react-native project' }
|
|
@@ -199,11 +181,11 @@ export class ToolsManage {
|
|
|
199
181
|
const chosen = platform || 'android'
|
|
200
182
|
if (chosen === 'android') {
|
|
201
183
|
const android = new AndroidManage()
|
|
202
|
-
const artifact = await
|
|
184
|
+
const artifact = await android.build(projectPath, { variant })
|
|
203
185
|
return artifact
|
|
204
186
|
} else {
|
|
205
187
|
const ios = new iOSManage()
|
|
206
|
-
const artifact = await
|
|
188
|
+
const artifact = await ios.build(projectPath, { variant })
|
|
207
189
|
return artifact
|
|
208
190
|
}
|
|
209
191
|
}
|
|
@@ -214,7 +196,7 @@ export class ToolsManage {
|
|
|
214
196
|
throw new Error('Both platform and projectType parameters are required (platform: ios|android, projectType: native|kmp|react-native|flutter).')
|
|
215
197
|
}
|
|
216
198
|
|
|
217
|
-
const chosenPlatform: 'android'|'ios' = platform
|
|
199
|
+
const chosenPlatform: 'android' | 'ios' = platform
|
|
218
200
|
|
|
219
201
|
if (chosenPlatform === 'android') {
|
|
220
202
|
const resolved = await resolveTargetDevice({ platform: 'android', deviceId })
|
|
@@ -271,7 +253,7 @@ export class ToolsManage {
|
|
|
271
253
|
|
|
272
254
|
static async buildAndInstallHandler({ platform, projectPath, deviceId, timeout, projectType }: { platform: 'android' | 'ios', projectPath: string, deviceId?: string, timeout?: number, projectType: 'native' | 'kmp' | 'react-native' | 'flutter' }) {
|
|
273
255
|
const events: string[] = []
|
|
274
|
-
const pushEvent = (obj:
|
|
256
|
+
const pushEvent = (obj: unknown) => events.push(JSON.stringify(obj))
|
|
275
257
|
const effectiveTimeout = timeout ?? 180000 // reserved for future streaming/timeouts
|
|
276
258
|
void effectiveTimeout
|
|
277
259
|
|
|
@@ -281,56 +263,19 @@ export class ToolsManage {
|
|
|
281
263
|
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Both platform and projectType parameters are required (platform: ios|android, projectType: native|kmp|react-native|flutter).' } }
|
|
282
264
|
}
|
|
283
265
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
try {
|
|
287
|
-
if (!chosenPlatform) {
|
|
288
|
-
// If caller provided projectType, respect it as a hard override and map to platform
|
|
289
|
-
if (projectType) {
|
|
290
|
-
if (projectType === 'kmp' || projectType === 'react-native' || projectType === 'flutter') {
|
|
291
|
-
chosenPlatform = 'android'
|
|
292
|
-
pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing android platform` })
|
|
293
|
-
} else if (projectType === 'native' || projectType === 'ios') {
|
|
294
|
-
chosenPlatform = 'ios'
|
|
295
|
-
pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing ios platform` })
|
|
296
|
-
} else {
|
|
297
|
-
pushEvent({ type: 'build', status: 'failed', error: `Unknown projectType: ${projectType}` })
|
|
298
|
-
return { ndjson: events.join('\n') + '\n', result: { success: false, error: `Unknown projectType: ${projectType}` } }
|
|
299
|
-
}
|
|
300
|
-
} else {
|
|
301
|
-
// If autodetect is disabled, require explicit platform or projectType
|
|
302
|
-
if (process.env.MCP_DISABLE_AUTODETECT === '1') {
|
|
303
|
-
pushEvent({ type: 'build', status: 'failed', error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType' })
|
|
304
|
-
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType (ios|android).' } }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const det = await detectProjectPlatform(projectPath)
|
|
308
|
-
if (det === 'ios' || det === 'android') {
|
|
309
|
-
chosenPlatform = det
|
|
310
|
-
} else if (det === 'ambiguous') {
|
|
311
|
-
pushEvent({ type: 'build', status: 'failed', error: 'Ambiguous project (contains both iOS and Android). Please provide platform: "ios" or "android".' })
|
|
312
|
-
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Ambiguous project - please provide explicit platform parameter (ios|android).' } }
|
|
313
|
-
} else {
|
|
314
|
-
// Unknown project type - do not guess. Request explicit platform.
|
|
315
|
-
pushEvent({ type: 'build', status: 'failed', error: 'Unknown project type - unable to autodetect platform. Please provide platform or projectType.' })
|
|
316
|
-
return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Unknown project type - please provide platform or projectType (ios|android).' } }
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
} catch {
|
|
321
|
-
// detection failed; avoid guessing a platform
|
|
322
|
-
}
|
|
266
|
+
// Explicit platform/projectType were required above; no autodetection occurs.
|
|
267
|
+
const chosenPlatform = platform
|
|
323
268
|
|
|
324
269
|
pushEvent({ type: 'build', status: 'started', platform: chosenPlatform })
|
|
325
270
|
|
|
326
|
-
let buildRes:
|
|
271
|
+
let buildRes: { artifactPath?: string; error?: string; output?: string; diagnostics?: unknown }
|
|
327
272
|
try {
|
|
328
|
-
buildRes = await ToolsManage.buildAppHandler({ platform: chosenPlatform
|
|
329
|
-
if (buildRes &&
|
|
330
|
-
pushEvent({ type: 'build', status: 'failed', error:
|
|
331
|
-
return { ndjson: events.join('\n') + '\n', result: { success: false, error:
|
|
273
|
+
buildRes = await ToolsManage.buildAppHandler({ platform: chosenPlatform, projectPath })
|
|
274
|
+
if (buildRes && buildRes.error) {
|
|
275
|
+
pushEvent({ type: 'build', status: 'failed', error: buildRes.error })
|
|
276
|
+
return { ndjson: events.join('\n') + '\n', result: { success: false, error: buildRes.error } }
|
|
332
277
|
}
|
|
333
|
-
pushEvent({ type: 'build', status: 'finished', artifactPath:
|
|
278
|
+
pushEvent({ type: 'build', status: 'finished', artifactPath: buildRes.artifactPath })
|
|
334
279
|
} catch (e) {
|
|
335
280
|
const msg = e instanceof Error ? e.message : String(e)
|
|
336
281
|
pushEvent({ type: 'build', status: 'failed', error: msg })
|
|
@@ -338,11 +283,11 @@ export class ToolsManage {
|
|
|
338
283
|
}
|
|
339
284
|
|
|
340
285
|
// Install phase
|
|
341
|
-
const artifact =
|
|
286
|
+
const artifact = buildRes.artifactPath || projectPath
|
|
342
287
|
pushEvent({ type: 'install', status: 'started', artifactPath: artifact, deviceId })
|
|
343
|
-
let installRes:
|
|
288
|
+
let installRes: InstallAppResponse
|
|
344
289
|
try {
|
|
345
|
-
installRes = await ToolsManage.installAppHandler({ platform: chosenPlatform
|
|
290
|
+
installRes = await ToolsManage.installAppHandler({ platform: chosenPlatform, appPath: artifact, deviceId, projectType })
|
|
346
291
|
if (installRes && installRes.installed === true) {
|
|
347
292
|
pushEvent({ type: 'install', status: 'finished', artifactPath: artifact, device: installRes.device })
|
|
348
293
|
return { ndjson: events.join('\n') + '\n', result: { success: true, artifactPath: artifact, device: installRes.device, output: installRes.output } }
|
|
@@ -358,7 +303,32 @@ export class ToolsManage {
|
|
|
358
303
|
}
|
|
359
304
|
|
|
360
305
|
static async listDevicesHandler({ platform, appId }: { platform?: 'android' | 'ios', appId?: string }) {
|
|
361
|
-
const devices = await listDevices(platform
|
|
306
|
+
const devices = await listDevices(platform, appId)
|
|
362
307
|
return { devices }
|
|
363
308
|
}
|
|
364
309
|
}
|
|
310
|
+
|
|
311
|
+
async function buildFlutterNative(projectPath: string, platform?: 'android' | 'ios', _forceClean?: boolean) {
|
|
312
|
+
// Fallback: try native subproject builds
|
|
313
|
+
if (!platform || platform === 'android') {
|
|
314
|
+
const androidDir = path.join(projectPath, 'android')
|
|
315
|
+
const android = new AndroidManage()
|
|
316
|
+
const artifact = await android.build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug')
|
|
317
|
+
return artifact
|
|
318
|
+
}
|
|
319
|
+
if (!platform || platform === 'ios') {
|
|
320
|
+
const iosDir = path.join(projectPath, 'ios')
|
|
321
|
+
const ios = new iOSManage()
|
|
322
|
+
const artifact = await ios.build(iosDir)
|
|
323
|
+
return artifact
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { error: 'Unable to build flutter project' }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function flutterBuildError(thrown: unknown, kind: string): Error {
|
|
330
|
+
const err = thrown as { stdout?: unknown; stderr?: unknown; message?: string }
|
|
331
|
+
const stdout = err && err.stdout ? String(err.stdout) : ''
|
|
332
|
+
const stderr = err && err.stderr ? String(err.stderr) : ''
|
|
333
|
+
return new Error(`flutter build ${kind} failed: ${stderr || stdout || err.message}`)
|
|
334
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type BuildEnv = Record<string, string | undefined>
|
|
2
|
+
|
|
3
|
+
export interface IOSBuildOptions {
|
|
4
|
+
variant?: string
|
|
5
|
+
workspace?: string
|
|
6
|
+
project?: string
|
|
7
|
+
scheme?: string
|
|
8
|
+
destinationUDID?: string
|
|
9
|
+
derivedDataPath?: string
|
|
10
|
+
buildJobs?: number
|
|
11
|
+
forceClean?: boolean
|
|
12
|
+
xcodeCmd?: string
|
|
13
|
+
env?: BuildEnv
|
|
14
|
+
}
|