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
package/scripts/agent-plugin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
-
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { fileURLToPath } from 'node:url'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
|
|
@@ -43,22 +43,53 @@ const cursorManifest = {
|
|
|
43
43
|
},
|
|
44
44
|
},
|
|
45
45
|
}
|
|
46
|
+
const antigravityManifest = {
|
|
47
|
+
$schema: 'https://antigravity.google/schemas/v1/plugin.json',
|
|
48
|
+
...sharedMetadata,
|
|
49
|
+
displayName: extensions['com.openai'].interface.displayName,
|
|
50
|
+
logo: 'assets/mobile-debug-tools.svg',
|
|
51
|
+
suggestedPrompts: extensions['com.openai'].interface.defaultPrompt,
|
|
52
|
+
}
|
|
53
|
+
const antigravityMcp = {
|
|
54
|
+
mcpServers: Object.fromEntries(Object.entries(codexMcp.mcpServers).map(([name, server]) => {
|
|
55
|
+
const { type: _transport, ...settings } = server
|
|
56
|
+
return [name, settings]
|
|
57
|
+
})),
|
|
58
|
+
}
|
|
59
|
+
const mcpConfigs = [
|
|
60
|
+
['.mcp.json', codexMcp],
|
|
61
|
+
['mcp_config.json', antigravityMcp],
|
|
62
|
+
]
|
|
46
63
|
const manifests = [
|
|
47
64
|
['.codex-plugin/plugin.json', codexManifest],
|
|
48
65
|
['.claude-plugin/plugin.json', claudeManifest],
|
|
49
66
|
['.cursor-plugin/plugin.json', cursorManifest],
|
|
67
|
+
['.antigravity-plugin/plugin.json', antigravityManifest],
|
|
50
68
|
]
|
|
51
69
|
|
|
70
|
+
const cursorFiles = [
|
|
71
|
+
['.cursor-plugin/plugin.json', `${JSON.stringify(cursorManifest, null, 2)}\n`],
|
|
72
|
+
['skills/mobile-debug-mcp/SKILL.md', readFileSync(packagedSkill)],
|
|
73
|
+
['LICENSE', readFileSync(sourceLicense)],
|
|
74
|
+
['README.md', '# Mobile Debug Tools for Cursor\n\nGenerated by `npm run sync:agent-plugin`; edit the canonical plugin source.\n\nCopy the contents of this directory into `~/.cursor/plugins/local/mobile-debug-tools`,\nthen run Developer: Reload Window and check Customize for the skill and MCP server.\nDo not copy the portable parent plugin: its `${PLUGIN_DATA}` variable is not expanded by Cursor.\n\nSetup and support: https://github.com/clivejefferies/mobile-debug-tools\n'],
|
|
75
|
+
]
|
|
52
76
|
if (process.argv[2] === '--sync') {
|
|
53
77
|
copyFileSync(sourceLicense, packagedLicense)
|
|
54
|
-
|
|
78
|
+
for (const [relativePath, contents] of cursorFiles) {
|
|
79
|
+
const target = path.join(pluginRoot, 'cursor', relativePath)
|
|
80
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
81
|
+
writeFileSync(target, contents)
|
|
82
|
+
}
|
|
83
|
+
for (const [relativePath, config] of mcpConfigs) {
|
|
84
|
+
writeFileSync(path.join(pluginRoot, relativePath), `${JSON.stringify(config, null, 2)}\n`)
|
|
85
|
+
}
|
|
55
86
|
for (const [relativePath, manifest] of manifests) {
|
|
56
87
|
const manifestPath = path.join(pluginRoot, relativePath)
|
|
57
88
|
mkdirSync(path.dirname(manifestPath), { recursive: true })
|
|
58
89
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
59
90
|
}
|
|
60
91
|
}
|
|
61
|
-
else if (process.argv[2]
|
|
92
|
+
else if (!['--check', '--export-antigravity'].includes(process.argv[2])) throw new Error('Use --sync, --check or --export-antigravity <new-directory>')
|
|
62
93
|
|
|
63
94
|
assert.equal(plugin.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json')
|
|
64
95
|
assert.equal(plugin.name, 'mobile-debug-tools')
|
|
@@ -73,7 +104,10 @@ assert.equal(server.command, 'npx')
|
|
|
73
104
|
assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
|
|
74
105
|
assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
|
|
75
106
|
assert.deepEqual(readFileSync(packagedLicense), readFileSync(sourceLicense), 'packaged license must match its source')
|
|
76
|
-
|
|
107
|
+
for (const [relativePath, config] of mcpConfigs) {
|
|
108
|
+
assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')), config, `${relativePath} must match the portable MCP configuration`)
|
|
109
|
+
}
|
|
110
|
+
assert.ok(readFileSync(path.join(pluginRoot, antigravityManifest.logo)).length > 0, 'Antigravity logo must be bundled')
|
|
77
111
|
for (const [relativePath, expectedManifest] of manifests) {
|
|
78
112
|
assert.deepEqual(
|
|
79
113
|
JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
|
|
@@ -81,6 +115,9 @@ for (const [relativePath, expectedManifest] of manifests) {
|
|
|
81
115
|
`${relativePath} must match the portable plugin`,
|
|
82
116
|
)
|
|
83
117
|
}
|
|
118
|
+
for (const [relativePath, contents] of cursorFiles) {
|
|
119
|
+
assert.deepEqual(readFileSync(path.join(pluginRoot, 'cursor', relativePath)), Buffer.from(contents), `Cursor native ${relativePath} must match its source`)
|
|
120
|
+
}
|
|
84
121
|
for (const marketplacePath of [
|
|
85
122
|
'.agents/plugins/marketplace.json',
|
|
86
123
|
'.claude-plugin/marketplace.json',
|
|
@@ -90,9 +127,22 @@ for (const marketplacePath of [
|
|
|
90
127
|
assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
|
|
91
128
|
assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
|
|
92
129
|
assert.equal(marketplace.plugins[0].name, plugin.name, `${marketplacePath} plugin name must match`)
|
|
93
|
-
assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, './agent-plugin', `${marketplacePath} source must point to the plugin`)
|
|
130
|
+
assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, marketplacePath.startsWith('.cursor-plugin') ? './agent-plugin/cursor' : './agent-plugin', `${marketplacePath} source must point to the plugin`)
|
|
94
131
|
}
|
|
95
132
|
const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
|
|
96
133
|
assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
|
|
97
134
|
|
|
98
135
|
console.log('Agent plugin package is in sync')
|
|
136
|
+
|
|
137
|
+
if (process.argv[2] === '--export-antigravity') {
|
|
138
|
+
assert.ok(process.argv[3], 'Supply a new output directory for the Antigravity export')
|
|
139
|
+
const target = path.resolve(process.argv[3])
|
|
140
|
+
// Refuse existing directories rather than overwrite source or stale package files.
|
|
141
|
+
mkdirSync(target)
|
|
142
|
+
writeFileSync(path.join(target, 'plugin.json'), `${JSON.stringify(antigravityManifest, null, 2)}\n`)
|
|
143
|
+
writeFileSync(path.join(target, 'mcp_config.json'), `${JSON.stringify(antigravityMcp, null, 2)}\n`)
|
|
144
|
+
copyFileSync(path.join(pluginRoot, '.antigravity-plugin/README.md'), path.join(target, 'README.md'))
|
|
145
|
+
copyFileSync(packagedLicense, path.join(target, 'LICENSE'))
|
|
146
|
+
for (const directory of ['skills', 'assets']) cpSync(path.join(pluginRoot, directory), path.join(target, directory), { recursive: true, filter: file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._') })
|
|
147
|
+
console.log(`Antigravity package exported to ${target}`)
|
|
148
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { isBuiltin } from 'node:module'
|
|
4
|
+
import { extractGraph } from 'archunit'
|
|
5
|
+
|
|
6
|
+
// ArchUnitTS 2.5.4 extracts static imports. Add re-exports, import types and
|
|
7
|
+
// literal dynamic imports using the same project's TypeScript resolution.
|
|
8
|
+
export async function architectureGraph(configFile = 'tsconfig.json') {
|
|
9
|
+
const configPath = path.resolve(configFile)
|
|
10
|
+
const root = path.dirname(configPath)
|
|
11
|
+
const config = ts.readConfigFile(configPath, ts.sys.readFile)
|
|
12
|
+
if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
|
|
13
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
|
|
14
|
+
if (parsed.errors.length) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
|
|
15
|
+
getCanonicalFileName: file => file, getCurrentDirectory: () => root, getNewLine: () => '\n'
|
|
16
|
+
}))
|
|
17
|
+
const graph = await extractGraph(configPath, { clearCache: true })
|
|
18
|
+
const local = file => path.relative(root, file).split(path.sep).join('/')
|
|
19
|
+
const edges = new Map(graph.map(edge => [JSON.stringify([edge.source, edge.target]), edge]))
|
|
20
|
+
const recorded = new Set()
|
|
21
|
+
for (const file of parsed.fileNames) {
|
|
22
|
+
const code = ts.sys.readFile(file)
|
|
23
|
+
if (code === undefined) throw new Error(`Cannot read architecture source: ${file}`)
|
|
24
|
+
const ast = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true)
|
|
25
|
+
const source = local(file)
|
|
26
|
+
function add(specifier, typeOnly = false) {
|
|
27
|
+
const module = specifier.text
|
|
28
|
+
const resolved = ts.resolveModuleName(module, file, parsed.options, ts.sys).resolvedModule
|
|
29
|
+
if (!resolved && !isBuiltin(module)) throw new Error(`Unresolved dependency: ${source} -> ${module}`)
|
|
30
|
+
const external = !resolved || !!resolved.isExternalLibraryImport
|
|
31
|
+
const target = resolved ? local(resolved.resolvedFileName) : module.replace(/^(?!node:)/, 'node:')
|
|
32
|
+
const key = JSON.stringify([source, target])
|
|
33
|
+
const kind = typeOnly ? 'type' : 'value'
|
|
34
|
+
const previous = edges.get(key)
|
|
35
|
+
// Replace the initial static classification, then union repeated dependencies.
|
|
36
|
+
if (previous && recorded.has(key)) {
|
|
37
|
+
previous.importKinds = [...new Set([...previous.importKinds, kind])]
|
|
38
|
+
} else edges.set(key, { source, target, external, importKinds: [kind] })
|
|
39
|
+
recorded.add(key)
|
|
40
|
+
}
|
|
41
|
+
function visit(node) {
|
|
42
|
+
if (ts.isImportDeclaration(node)) {
|
|
43
|
+
const clause = node.importClause
|
|
44
|
+
const bindings = clause?.namedBindings
|
|
45
|
+
const typeOnly = clause?.isTypeOnly || (!clause?.name && bindings && ts.isNamedImports(bindings) &&
|
|
46
|
+
bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
|
|
47
|
+
add(node.moduleSpecifier, !!typeOnly)
|
|
48
|
+
} else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
|
|
49
|
+
const bindings = node.exportClause
|
|
50
|
+
const typeOnly = node.isTypeOnly || (bindings && ts.isNamedExports(bindings) &&
|
|
51
|
+
bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
|
|
52
|
+
add(node.moduleSpecifier, !!typeOnly)
|
|
53
|
+
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
|
54
|
+
if (!node.moduleReference.expression) throw new Error(`Missing import target in ${source}`)
|
|
55
|
+
add(node.moduleReference.expression, node.isTypeOnly)
|
|
56
|
+
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
57
|
+
add(node.argument.literal, true)
|
|
58
|
+
} else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
|
|
59
|
+
(ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
|
|
60
|
+
const argument = node.arguments[0]
|
|
61
|
+
if (!argument || !ts.isStringLiteralLike(argument)) throw new Error(`Nonliteral dependency in ${source}; architecture cannot resolve it`)
|
|
62
|
+
add(argument)
|
|
63
|
+
}
|
|
64
|
+
ts.forEachChild(node, visit)
|
|
65
|
+
}
|
|
66
|
+
visit(ast)
|
|
67
|
+
}
|
|
68
|
+
return [...edges.values()]
|
|
69
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { gatherDependOnFileViolations, gatherCycleViolations, projectEdges, perEdge, perInternalEdge } from 'archunit'
|
|
4
|
+
import { architectureGraph } from './architecture-graph.mjs'
|
|
5
|
+
|
|
6
|
+
export const architectureRules = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Shared contracts stay independent of application implementations',
|
|
9
|
+
source: /^src\/contracts\//,
|
|
10
|
+
forbidden: /^src\/(?!contracts\/)/
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Capabilities and shared modules cannot import the MCP server layer',
|
|
14
|
+
source: /^src\/(?!server(?:\/|-core\.ts$|\.ts$))/,
|
|
15
|
+
forbidden: /^src\/server(?:\/|-core\.ts$|\.ts$)/
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Pure interaction calculations cannot import native I/O or service facades',
|
|
19
|
+
source: /^src\/interact\/(?:adjustment\/(?:adjustment-plan|control-values|control-geometry|slider-labels)|targeting\/(?:candidate-scoring|candidate-ancestor|candidate-result|nearby-control|element-helpers|resolved-elements|tap-target|target-scope)|waits\/(?:ui-change-helpers|ui-wait-condition)|assertions\/state-comparison)\.ts$/,
|
|
20
|
+
forbidden: /^(?:node:(?:child_process|fs|net|http|https)(?:\/|$)|src\/(?:utils\/(?:android|ios)\/|(?:interact|observe|manage)\/(?:index|android|ios)\.ts$))/
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
const filter = regExp => ({ regExp, options: { target: 'path', matching: 'partial' } })
|
|
24
|
+
|
|
25
|
+
export function checkArchitecture(graph) {
|
|
26
|
+
const projected = projectEdges(graph, perEdge())
|
|
27
|
+
const results = architectureRules.map(rule => ({
|
|
28
|
+
name: rule.name,
|
|
29
|
+
violations: gatherDependOnFileViolations(projected, [filter(rule.source)], [filter(rule.forbidden)], true)
|
|
30
|
+
}))
|
|
31
|
+
const runtime = graph.filter(edge => edge.importKinds.includes('value') || !edge.importKinds.includes('type'))
|
|
32
|
+
results.push({ name: 'Production runtime dependencies must be cycle-free',
|
|
33
|
+
violations: gatherCycleViolations(projectEdges(runtime, perInternalEdge()), []) })
|
|
34
|
+
return results
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const graph = await architectureGraph(process.argv[2])
|
|
39
|
+
const results = checkArchitecture(graph)
|
|
40
|
+
const failures = results.filter(result => result.violations.length)
|
|
41
|
+
for (const result of failures) console.error(`${result.name}\n${JSON.stringify(result.violations, null, 2)}`)
|
|
42
|
+
if (failures.length) process.exitCode = 1
|
|
43
|
+
else console.log(`Architecture: PASS (${results.length} rules; ${graph.filter(edge => edge.source !== edge.target).length} dependency edges, including type, dynamic and re-export dependencies)`)
|
|
44
|
+
}
|
|
45
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
46
|
+
main().catch(error => { console.error(error); process.exitCode = 1 })
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { ESLint } from 'eslint'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import ts from 'typescript'
|
|
4
|
+
import { parse } from '@typescript-eslint/parser'
|
|
5
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
export const limits = Object.freeze({ 'max-lines': 500, 'class-lines': 300,
|
|
10
|
+
'max-lines-per-function': 80, complexity: 20, 'max-depth': 4, 'explicit-any': 0 })
|
|
11
|
+
const functionKinds = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
|
|
12
|
+
const measuredRules = new Set(['max-lines', 'max-lines-per-function', 'complexity', 'max-depth'])
|
|
13
|
+
|
|
14
|
+
function children(node) {
|
|
15
|
+
return Object.entries(node).filter(([key]) => !['parent', 'tokens', 'comments', 'loc', 'range'].includes(key))
|
|
16
|
+
.flatMap(([, value]) => Array.isArray(value) ? value : [value])
|
|
17
|
+
.filter(value => value && typeof value === 'object' && typeof value.type === 'string')
|
|
18
|
+
}
|
|
19
|
+
function label(node, parent) {
|
|
20
|
+
if (node.id?.name) return node.id.name
|
|
21
|
+
if (parent?.type === 'MethodDefinition' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition') {
|
|
22
|
+
const name = parent.key.name ?? parent.key.value ?? '[computed]'
|
|
23
|
+
return ['get', 'set'].includes(parent.kind) ? `${parent.kind}:${name}` : name
|
|
24
|
+
}
|
|
25
|
+
if (parent?.type === 'VariableDeclarator') return parent.id.name ?? '[destructured]'
|
|
26
|
+
if (parent?.type === 'CallExpression' || parent?.type === 'NewExpression') {
|
|
27
|
+
const callee = parent.callee.name ?? parent.callee.property?.name ?? '[call]'
|
|
28
|
+
return `${callee}:argument${parent.arguments.indexOf(node)}`
|
|
29
|
+
}
|
|
30
|
+
return `[${node.type}]`
|
|
31
|
+
}
|
|
32
|
+
function syntax(code) {
|
|
33
|
+
const ast = parse(code, { loc: true, range: true, comment: true, sourceType: 'module', ecmaVersion: 'latest' })
|
|
34
|
+
const symbols = [], anys = [], imports = []
|
|
35
|
+
function visit(node, parent, scope = '<module>') {
|
|
36
|
+
if (['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(node.type)) scope = `${scope}/${node.id.name}`
|
|
37
|
+
if (functionKinds.has(node.type) || node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
38
|
+
const anonymous = !node.id?.name && !['MethodDefinition', 'Property', 'PropertyDefinition', 'VariableDeclarator'].includes(parent?.type)
|
|
39
|
+
const suffix = anonymous ? ':' + createHash('sha256').update(code.slice(...node.range).replace(/\s/g, '')).digest('hex').slice(0, 12) : ''
|
|
40
|
+
scope = `${scope}/${label(node, parent)}${suffix}`
|
|
41
|
+
symbols.push({ node, identity: scope })
|
|
42
|
+
}
|
|
43
|
+
if (node.type === 'TSAnyKeyword') anys.push({ node, identity: scope })
|
|
44
|
+
if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration', 'ImportExpression', 'TSImportType'].includes(node.type) && node.source) {
|
|
45
|
+
imports.push({ node, value: node.source.value })
|
|
46
|
+
}
|
|
47
|
+
if (node.type === 'CallExpression' && node.callee.name === 'require' && node.arguments[0]?.type === 'Literal') {
|
|
48
|
+
imports.push({ node, value: node.arguments[0].value })
|
|
49
|
+
}
|
|
50
|
+
for (const child of children(node)) visit(child, node, scope)
|
|
51
|
+
}
|
|
52
|
+
visit(ast, null)
|
|
53
|
+
return { ast, symbols, anys, imports }
|
|
54
|
+
}
|
|
55
|
+
function lineCount(code, comments, start = 0, end = code.length) {
|
|
56
|
+
const chars = code.split('')
|
|
57
|
+
for (const c of comments) for (let i = c.range[0]; i < c.range[1]; i++) if (chars[i] !== '\n') chars[i] = ' '
|
|
58
|
+
return chars.slice(start, end).join('').split('\n').filter(line => line.trim()).length
|
|
59
|
+
}
|
|
60
|
+
const eslint = new ESLint({ overrideConfig: [{ files: ['**/*.ts'], linterOptions: { noInlineConfig: true }, rules: {
|
|
61
|
+
'max-lines': ['error', { max: limits['max-lines'], skipBlankLines: true, skipComments: true }],
|
|
62
|
+
'max-lines-per-function': ['error', { max: limits['max-lines-per-function'], skipBlankLines: true, skipComments: true }],
|
|
63
|
+
complexity: ['error', { max: limits.complexity, variant: 'classic' }], 'max-depth': ['error', limits['max-depth']]
|
|
64
|
+
} }] })
|
|
65
|
+
|
|
66
|
+
export async function measure(file, code) {
|
|
67
|
+
if (file.endsWith('.java')) {
|
|
68
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, code)
|
|
69
|
+
const comments = []
|
|
70
|
+
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
71
|
+
if ([ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia].includes(token)) {
|
|
72
|
+
comments.push({ range: [scanner.getTokenPos(), scanner.getTextPos()] })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const value = lineCount(code, comments)
|
|
76
|
+
return value > limits['max-lines'] ? [{ file, rule: 'max-lines', symbol: '<module>', value, line: 1 }] : []
|
|
77
|
+
}
|
|
78
|
+
const { ast, symbols, anys, imports } = syntax(code)
|
|
79
|
+
const findings = []
|
|
80
|
+
const add = (rule, symbol, value, line) => findings.push({ file, rule, symbol, value, line })
|
|
81
|
+
const identities = new Set()
|
|
82
|
+
for (const { node, identity } of symbols) {
|
|
83
|
+
if (identities.has(identity) && !identity.includes(':argument')) add('duplicate-symbol', identity, 1, node.loc.start.line)
|
|
84
|
+
identities.add(identity)
|
|
85
|
+
if (node.type.startsWith('Class')) {
|
|
86
|
+
const value = lineCount(code, ast.comments, ...node.range)
|
|
87
|
+
if (value > limits['class-lines']) add('class-lines', identity, value, node.loc.start.line)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const anyCounts = new Map()
|
|
91
|
+
for (const { node, identity } of anys) {
|
|
92
|
+
const current = anyCounts.get(identity) ?? { value: 0, line: node.loc.start.line }
|
|
93
|
+
current.value++; anyCounts.set(identity, current)
|
|
94
|
+
}
|
|
95
|
+
for (const [identity, { value, line }] of anyCounts) add('explicit-any', identity, value, line)
|
|
96
|
+
const [result] = await eslint.lintText(code, { filePath: file })
|
|
97
|
+
for (const m of result.messages.filter(m => measuredRules.has(m.ruleId))) {
|
|
98
|
+
const value = Number(/(?:has too many lines \(|complexity of |nested too deeply \()([\d]+)/.exec(m.message)?.[1])
|
|
99
|
+
if (!Number.isFinite(value)) throw new Error(`Cannot read ${m.ruleId} metric: ${m.message}`)
|
|
100
|
+
const candidates = symbols.filter(({ node }) => m.line >= node.loc.start.line && m.line <= node.loc.end.line)
|
|
101
|
+
.sort((a, b) => a.node.range[1] - a.node.range[0] - (b.node.range[1] - b.node.range[0]))
|
|
102
|
+
const symbol = m.ruleId === 'max-lines' ? '<module>' : candidates[0]?.identity ?? '<module>'
|
|
103
|
+
add(m.ruleId, symbol, value, m.line)
|
|
104
|
+
}
|
|
105
|
+
for (const comment of ast.comments) {
|
|
106
|
+
if (/(?:eslint-(?:disable|enable)|^\s*eslint\s)/.test(comment.value) &&
|
|
107
|
+
(!/eslint-(?:disable|enable)(?:-next-line|-line)?\s+(?!-)[\w@]/.test(comment.value) ||
|
|
108
|
+
/max-lines|complexity|max-depth|no-explicit-any|code-quality/.test(comment.value))) {
|
|
109
|
+
add('inline-suppression', '<module>', 1, comment.loc.start.line)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (file.startsWith('src/interact/') || file.startsWith('src/runtime/')) {
|
|
113
|
+
for (const { node, value } of imports) if (typeof value === 'string' && /(?:^|\/)server(?:\/|-core)/.test(value)) {
|
|
114
|
+
add('import-boundary', '<module>', 1, node.loc.start.line)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Multiple nested blocks can report depth in the same function; retain its maximum.
|
|
118
|
+
return [...findings.reduce((out, f) => {
|
|
119
|
+
const key = `${f.rule}:${f.symbol}`
|
|
120
|
+
if (!out.has(key) || out.get(key).value < f.value) out.set(key, f)
|
|
121
|
+
return out
|
|
122
|
+
}, new Map()).values()]
|
|
123
|
+
}
|
|
124
|
+
export function compare(findings, baseline) {
|
|
125
|
+
const errors = [], entries = new Map()
|
|
126
|
+
const key = f => JSON.stringify([f.file, f.rule, f.symbol])
|
|
127
|
+
for (const b of baseline) {
|
|
128
|
+
if (!b.file || !b.rule || !b.symbol || !b.reason || !Number.isInteger(b.value) || b.value < 1) {
|
|
129
|
+
errors.push('Invalid baseline entry'); continue
|
|
130
|
+
}
|
|
131
|
+
if (entries.has(key(b))) errors.push(`Duplicate baseline entry: ${key(b)}`)
|
|
132
|
+
entries.set(key(b), b)
|
|
133
|
+
}
|
|
134
|
+
for (const f of findings) {
|
|
135
|
+
const b = entries.get(key(f))
|
|
136
|
+
if (['duplicate-symbol', 'inline-suppression', 'import-boundary'].includes(f.rule) || !b || f.value > b.value) {
|
|
137
|
+
errors.push(`${f.file}:${f.line}: ${f.rule} ${f.symbol} = ${f.value}; allowed ${b?.value ?? limits[f.rule] ?? 0}`)
|
|
138
|
+
} else if (f.value < b.value) errors.push(`Lower baseline ceiling for ${key(f)}: ${b.value} -> ${f.value}`)
|
|
139
|
+
entries.delete(key(f))
|
|
140
|
+
}
|
|
141
|
+
for (const b of entries.values()) errors.push(`Remove obsolete baseline entry: ${key(b)}`)
|
|
142
|
+
return errors
|
|
143
|
+
}
|
|
144
|
+
export async function collect(root = 'src') {
|
|
145
|
+
const out = []
|
|
146
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
147
|
+
const file = path.posix.join(root, entry.name)
|
|
148
|
+
if (entry.isDirectory()) out.push(...await collect(file))
|
|
149
|
+
else if (/\.(ts|java)$/.test(file)) out.push(...await measure(file, await readFile(file, 'utf8')))
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
async function main() {
|
|
154
|
+
const baseline = JSON.parse(await readFile(new URL('./code-quality-baseline.json', import.meta.url), 'utf8'))
|
|
155
|
+
const errors = compare(await collect(), baseline)
|
|
156
|
+
if (errors.length) { console.error(errors.join('\n')); process.exitCode = 1 }
|
|
157
|
+
else console.log('Code quality: PASS (size, complexity, nesting, types, boundaries and legacy ceilings)')
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
+
main().catch(error => { console.error(error); process.exitCode = 1 })
|
|
161
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const verificationModes = ['none', 'light', 'full'] as const
|
|
4
|
+
export const responseModes = ['compact', 'debug'] as const
|
|
5
|
+
export const verificationModeSchema = z.enum(verificationModes)
|
|
6
|
+
export const responseModeSchema = z.enum(responseModes)
|
|
7
|
+
export type VerificationMode = z.infer<typeof verificationModeSchema>
|
|
8
|
+
export type ResponseMode = z.infer<typeof responseModeSchema>
|
|
9
|
+
|
|
10
|
+
export const verificationModeJson = { type: 'string', enum: [...verificationModes] }
|
|
11
|
+
export const responseModeJson = { type: 'string', enum: [...responseModes] }
|
|
12
|
+
|
|
13
|
+
export function interactionControls(args: Record<string, unknown>) {
|
|
14
|
+
const verificationMode = ((typeof args.verificationMode === 'string' ? args.verificationMode : undefined) as VerificationMode | undefined) ?? 'full'
|
|
15
|
+
const responseMode = ((typeof args.responseMode === 'string' ? args.responseMode : undefined) as ResponseMode | undefined) ?? 'debug'
|
|
16
|
+
if (!verificationModes.includes(verificationMode) || !responseModes.includes(responseMode)) throw new Error('INVALID_ARGUMENT')
|
|
17
|
+
for (const key of ['verificationMode', 'responseMode']) if (args[key] !== undefined && typeof args[key] !== 'string') throw new Error('INVALID_ARGUMENT')
|
|
18
|
+
for (const key of ['actionTimeoutMs', 'verificationTimeoutMs']) if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key]))) throw new Error('INVALID_ARGUMENT')
|
|
19
|
+
const actionTimeoutMs = (typeof args.actionTimeoutMs === 'number' && Number.isFinite(args.actionTimeoutMs) ? args.actionTimeoutMs : undefined)
|
|
20
|
+
const verificationTimeoutMs = (typeof args.verificationTimeoutMs === 'number' && Number.isFinite(args.verificationTimeoutMs) ? args.verificationTimeoutMs : undefined)
|
|
21
|
+
return { verificationMode, responseMode,
|
|
22
|
+
actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
|
|
23
|
+
verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) }
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type UiSelector = {
|
|
4
|
+
text?: string
|
|
5
|
+
resource_id?: string
|
|
6
|
+
accessibility_id?: string
|
|
7
|
+
contains?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const selectorSchema = z.object({
|
|
11
|
+
text: z.string().min(1).optional(), resource_id: z.string().min(1).optional(),
|
|
12
|
+
accessibility_id: z.string().min(1).optional(), contains: z.boolean().optional()
|
|
13
|
+
}).strict().refine(value => !!(value.text || value.resource_id || value.accessibility_id), 'Selector requires a target')
|
|
14
|
+
|
|
15
|
+
export const selectorJson = { type: 'object', additionalProperties: false, properties: {
|
|
16
|
+
text: { type: 'string', minLength: 1 }, resource_id: { type: 'string', minLength: 1 }, accessibility_id: { type: 'string', minLength: 1 }, contains: { type: 'boolean' }
|
|
17
|
+
}, anyOf: ['text', 'resource_id', 'accessibility_id'].map(key => ({ required: [key] })) }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { selectorSchema as selector, selectorJson } from './selector.js'
|
|
3
|
+
|
|
4
|
+
export interface DeviceTarget { platform?: 'android' | 'ios'; deviceId?: string }
|
|
5
|
+
export type ScrollSelector = { text?: string; resourceId?: string; contentDesc?: string; className?: string }
|
|
6
|
+
const pollIntervalMs = z.number().int().min(50).max(1000).optional()
|
|
7
|
+
export const waitForSchema = z.object({
|
|
8
|
+
condition: z.enum(['exists', 'visible', 'clickable']).optional(),
|
|
9
|
+
timeoutMs: z.number().int().min(100).max(10000).optional(), pollIntervalMs,
|
|
10
|
+
match: z.object({ index: z.number().int().nonnegative().optional() }).strict().optional()
|
|
11
|
+
}).strict()
|
|
12
|
+
export const targetFields = { selector: selector.optional(), elementId: z.string().min(1).optional() }
|
|
13
|
+
export const exactlyOneTarget = (value: { selector?: unknown, elementId?: string }) => Number(!!value.selector) + Number(!!value.elementId) === 1
|
|
14
|
+
export const tapElementControlsSchema = z.object({
|
|
15
|
+
elementId: z.string().min(1).optional(), selector: selector.optional(), waitFor: waitForSchema.optional()
|
|
16
|
+
}).strict().refine(value => Number(!!value.elementId) + Number(!!value.selector) === 1 && (!value.elementId || !value.waitFor))
|
|
17
|
+
|
|
18
|
+
export const targetJson = { selector: selectorJson, elementId: { type: 'string', minLength: 1 } }
|
|
19
|
+
export const oneTargetJson = [{ required: ['selector'], not: { required: ['elementId'] } }, { required: ['elementId'], not: { required: ['selector'] } }]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
export interface UIElementState {
|
|
2
|
+
checked?: boolean | null;
|
|
3
|
+
selected?: boolean | string | { id: string; label?: string } | null;
|
|
4
|
+
focused?: boolean | null;
|
|
5
|
+
expanded?: boolean | null;
|
|
6
|
+
enabled?: boolean | null;
|
|
7
|
+
text_value?: string | null;
|
|
8
|
+
value?: number | string | null;
|
|
9
|
+
raw_value?: number | string | null;
|
|
10
|
+
value_range?: {
|
|
11
|
+
min: number;
|
|
12
|
+
max: number;
|
|
13
|
+
step?: number | null;
|
|
14
|
+
} | null;
|
|
15
|
+
step?: number | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SelectorConfidence {
|
|
19
|
+
score: number;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UIResolutionSelector {
|
|
24
|
+
value: string | null;
|
|
25
|
+
confidence: SelectorConfidence | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UIElementSemanticMetadata {
|
|
29
|
+
is_clickable: boolean;
|
|
30
|
+
is_container: boolean;
|
|
31
|
+
semantic_role?: 'slider' | 'stepper' | 'dropdown' | 'segmented_control' | 'custom_adjustable' | 'composite_control' | null;
|
|
32
|
+
supported_actions?: string[] | null;
|
|
33
|
+
adjustable?: boolean | null;
|
|
34
|
+
state_shape?: 'continuous' | 'discrete' | 'semantic' | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoadingState {
|
|
38
|
+
active: boolean;
|
|
39
|
+
signal: string;
|
|
40
|
+
source: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SnapshotDelta {
|
|
44
|
+
previous_snapshot_revision: number | null;
|
|
45
|
+
added_elements: number;
|
|
46
|
+
removed_elements: number;
|
|
47
|
+
mutated_elements: number;
|
|
48
|
+
total_elements: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface StateDelta {
|
|
52
|
+
base_snapshot_revision: number | null;
|
|
53
|
+
snapshot_revision: number;
|
|
54
|
+
added: UIElement[];
|
|
55
|
+
removed: Array<{ stable_id?: string; resourceId?: string; contentDescription?: string; text?: string; index: number }>;
|
|
56
|
+
changed: UIElement[];
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
omitted_changes?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface UIElement {
|
|
62
|
+
text: string | null;
|
|
63
|
+
contentDescription: string | null;
|
|
64
|
+
type: string;
|
|
65
|
+
clickable: boolean;
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
visible: boolean;
|
|
68
|
+
bounds: [number, number, number, number];
|
|
69
|
+
resourceId: string | null;
|
|
70
|
+
parentId?: number;
|
|
71
|
+
children?: number[];
|
|
72
|
+
center?: [number, number];
|
|
73
|
+
depth?: number;
|
|
74
|
+
state?: UIElementState | null;
|
|
75
|
+
stable_id?: string | null;
|
|
76
|
+
role?: string | null;
|
|
77
|
+
test_tag?: string | null;
|
|
78
|
+
selector?: UIResolutionSelector | null;
|
|
79
|
+
semantic?: UIElementSemanticMetadata | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ReadinessSummary {
|
|
83
|
+
state: 'loading' | 'transitioning' | 'settled' | 'unknown' | 'unresponsive' | 'debugger_paused';
|
|
84
|
+
scope: 'device_ui';
|
|
85
|
+
basis: 'observed' | 'heuristic' | 'insufficient';
|
|
86
|
+
captured_at_ms: number | null;
|
|
87
|
+
sample_count: number;
|
|
88
|
+
stable_for_ms: number;
|
|
89
|
+
snapshot_session_id: string | null;
|
|
90
|
+
snapshot_revision: number | null;
|
|
91
|
+
evidence: Array<{ code: string; source: string; captured_at_ms: number | null; snapshot_revision: number | null }>;
|
|
92
|
+
limitations: string[];
|
|
93
|
+
capabilities: { loading: boolean; transitioning: boolean; settled: boolean; unresponsive: boolean; debugger_paused: boolean };
|
|
94
|
+
}
|
package/src/debugger/android.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process'
|
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
4
|
import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
|
|
5
5
|
const exec = promisify(execFile)
|
|
6
|
-
export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
|
|
6
|
+
export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number | null; executable_path?: string }
|
|
7
7
|
export interface DebugNative {
|
|
8
8
|
candidates(device: string, app: string): Promise<DebugTarget[]>
|
|
9
9
|
identity(device: string, target: DebugTarget): Promise<DebugTarget>
|
|
@@ -34,7 +34,8 @@ export class AndroidDebugNative implements DebugNative {
|
|
|
34
34
|
// `adb jdwp` is a subscription, not a one-shot command. Bound the sample.
|
|
35
35
|
let jdwpOutput: string
|
|
36
36
|
try { jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout }
|
|
37
|
-
catch (
|
|
37
|
+
catch (error: unknown) {
|
|
38
|
+
const e = error as { killed?: boolean; stderr?: string; stdout?: string }
|
|
38
39
|
if (debuggerSignal()?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
|
|
39
40
|
debuggerRemaining(1)
|
|
40
41
|
if (!e.killed || e.stderr?.trim()) throw nativeError(e, 'DEVICE_UNAVAILABLE')
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Only catalog values cross the debugger error boundary. Never inspect exception text. */
|
|
2
|
+
export const stages = ['prerequisite', 'compilation', 'worker_start', 'ipc', 'target_resolution', 'forwarding', 'attach', 'command', 'transport', 'cleanup', 'unknown'] as const
|
|
3
|
+
export const reasons = ['unavailable', 'unsupported', 'spawn_failed', 'process_exited', 'invalid_message', 'message_limit', 'write_failed', 'connection_refused', 'transport_lost', 'timeout', 'cancelled', 'operation_failed', 'unknown'] as const
|
|
4
|
+
export type DiagnosticStage = typeof stages[number]
|
|
5
|
+
export type DiagnosticReason = typeof reasons[number]
|
|
6
|
+
export interface DebuggerDiagnostic { stage: DiagnosticStage; reason: DiagnosticReason; recovery: string }
|
|
7
|
+
const recovery: Record<DiagnosticStage, string> = {
|
|
8
|
+
prerequisite: 'Check JDK 17+ availability, including java, javac and jdk.jdi.',
|
|
9
|
+
compilation: 'Check the JDK compiler and worker source/cache availability.',
|
|
10
|
+
worker_start: 'Check Java worker launch availability. Inspect session state and cleanup before attaching again.',
|
|
11
|
+
ipc: 'Inspect session state and cleanup. Detach before attempting a fresh attach.',
|
|
12
|
+
target_resolution: 'Check the explicit device and running debuggable app/process.',
|
|
13
|
+
forwarding: 'Check the explicit device and debugger forwarding availability.',
|
|
14
|
+
attach: 'Check the running debuggable target and whether another debugger is attached. Inspect cleanup before a fresh attach.',
|
|
15
|
+
command: 'Inspect debugger session state and cleanup before another state-changing command.',
|
|
16
|
+
transport: 'Inspect session state and cleanup. Detach before attempting a fresh attach; execution state may be unknown.',
|
|
17
|
+
cleanup: 'Retry detach_debugger and inspect cleanup.remaining_resources. Do not kill the app or ADB server.',
|
|
18
|
+
unknown: 'Inspect debugger session state and cleanup; the failure cause is unknown.'
|
|
19
|
+
}
|
|
20
|
+
export function diagnostic(stage: DiagnosticStage, reason: DiagnosticReason): DebuggerDiagnostic {
|
|
21
|
+
const guidance = reason === 'timeout' || reason === 'cancelled'
|
|
22
|
+
? 'Execution may be uncertain. Inspect session state and cleanup before another state-changing command.' : recovery[stage]
|
|
23
|
+
return { stage, reason, recovery: guidance }
|
|
24
|
+
}
|
|
25
|
+
export function parseDiagnostic(value: unknown): DebuggerDiagnostic {
|
|
26
|
+
if (!value || typeof value !== 'object') throw new Error('INVALID_WORKER_DIAGNOSTIC')
|
|
27
|
+
const { stage, reason } = value as Record<string, unknown>
|
|
28
|
+
if (!stages.includes(stage as DiagnosticStage) || !reasons.includes(reason as DiagnosticReason)) throw new Error('INVALID_WORKER_DIAGNOSTIC')
|
|
29
|
+
return diagnostic(stage as DiagnosticStage, reason as DiagnosticReason)
|
|
30
|
+
}
|
|
31
|
+
export class DebuggerError extends Error {
|
|
32
|
+
constructor(public code: string, public diagnostic?: DebuggerDiagnostic) { super(code) }
|
|
33
|
+
}
|
|
34
|
+
const validationCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_PAUSED', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE'])
|
|
35
|
+
export function classify(error: unknown, stage: DiagnosticStage, fallback: string, reason: DiagnosticReason = 'unknown'): DebuggerError {
|
|
36
|
+
const existing = error instanceof DebuggerError ? error : new DebuggerError(fallback)
|
|
37
|
+
if (existing.diagnostic || validationCodes.has(existing.code)) return existing
|
|
38
|
+
const knownReason = existing.code === 'DEBUGGER_TIMEOUT' ? 'timeout' : existing.code === 'DEBUGGER_CANCELLED' ? 'cancelled' : reason
|
|
39
|
+
return new DebuggerError(existing.code, diagnostic(stage, knownReason))
|
|
40
|
+
}
|
|
41
|
+
export async function atStage<T>(stage: DiagnosticStage, fallback: string, operation: () => Promise<T>): Promise<T> {
|
|
42
|
+
try { return await operation() } catch (error) { throw classify(error, stage, fallback) }
|
|
43
|
+
}
|
package/src/debugger/guard.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
2
2
|
import { DebuggerError } from './worker.js'
|
|
3
|
-
|
|
3
|
+
interface PauseEvidence {
|
|
4
|
+
debug_session_id: string
|
|
5
|
+
captured_at_ms: number
|
|
6
|
+
target: unknown
|
|
7
|
+
}
|
|
8
|
+
let pauseProvider: (device: string, platform?: string) => PauseEvidence | null = () => null
|
|
4
9
|
export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
|
|
5
10
|
const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
|
|
6
11
|
export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
|
|
@@ -9,13 +14,13 @@ export function withDebuggerGuard<T>(platform: string, device: string, operation
|
|
|
9
14
|
// Synchronous checks and native submission share a JS turn with pause updates.
|
|
10
15
|
export function checkDebuggerGuard(dispatch = false) {
|
|
11
16
|
const current = context.getStore()
|
|
12
|
-
if (!current
|
|
13
|
-
if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
|
|
17
|
+
if (!current) return
|
|
18
|
+
if (pauseProvider(current.device, current.platform)) throw new DebuggerError('DEBUGGER_PAUSED')
|
|
14
19
|
if (dispatch) current.dispatched = true
|
|
15
20
|
}
|
|
16
21
|
export function guardDispatched() { return context.getStore()?.dispatched ?? false }
|
|
17
|
-
export function pausedReadiness(device: string) {
|
|
18
|
-
const paused = pauseProvider(device)
|
|
22
|
+
export function pausedReadiness(device: string, platform = 'android') {
|
|
23
|
+
const paused = pauseProvider(device, platform)
|
|
19
24
|
if (!paused) return null
|
|
20
25
|
return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
|
|
21
26
|
captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
|