mobile-debug-mcp 0.33.0 → 0.35.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.
Files changed (448) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +20 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,106 @@
1
+ import { systemClock } from '../../runtime/clock.js';
2
+ import { remainingBudget } from '../../utils/operation-budget.js';
3
+ import { checkDebuggerGuard } from '../../debugger/guard.js';
4
+ import { observeReadinessWait, isDeviceUnavailable } from '../../observe/readiness.js';
5
+ import { readinessDelay } from '../../observe/readiness-service.js';
6
+ import { matchingElements, evaluateCondition } from './ui-wait-condition.js';
7
+ import { createUIWait, validateUIWait, waitObserved, waitFailure } from './ui-wait-state.js';
8
+ async function delay(s, ms) {
9
+ await (s.tracker && s.clock === systemClock ? readinessDelay(ms) : s.clock.sleep(ms));
10
+ }
11
+ function acceptObservation(s, tree, now) {
12
+ const age = typeof tree?.captured_at_ms === 'number' ? now - tree.captured_at_ms : null;
13
+ if (!(s.met && (age === null || age <= 500))) {
14
+ s.stable = 0;
15
+ return null;
16
+ }
17
+ s.stable++;
18
+ if (s.stable < (s.args.singleObservation ? 1 : 2) || (s.tracker && s.tracker.summary.state !== 'settled'))
19
+ return null;
20
+ return { status: 'success', ...(s.args.singleObservation ? { _tree: tree } : {}), matched: s.count,
21
+ element: s.element, metrics: { latency_ms: now - s.start, poll_count: s.polls, attempts: s.attempts }, requested: s.requested, observed: waitObserved(s, true) };
22
+ }
23
+ function inspectObservation(s, tree, deps) {
24
+ const a = s.args, elements = tree && Array.isArray(tree.elements) ? tree.elements : [];
25
+ const matches = matchingElements(elements, a.selector);
26
+ if (a.rejectAmbiguous && matches.length > 1 && a.match?.index === undefined)
27
+ return { status: 'timeout', error: { code: 'AMBIGUOUS_TARGET' } };
28
+ const index = typeof a.match?.index === 'number' ? a.match.index : undefined;
29
+ const condition = evaluateCondition(elements, matches, a.condition, index, a.resolveTapTarget);
30
+ const platform = tree?.device?.platform === 'ios' ? 'ios' : a.platform || 'android';
31
+ const deviceId = tree?.device?.id || a.deviceId;
32
+ s.count = matches.length;
33
+ s.met = condition.met;
34
+ s.element = condition.element ? deps.resolve(platform, deviceId, condition.element.el, condition.element.idx) : null;
35
+ return acceptObservation(s, tree, s.clock.now());
36
+ }
37
+ function pollError(s, error) {
38
+ checkDebuggerGuard();
39
+ if (error instanceof Error && error.message === 'ACTION_TIMEOUT')
40
+ throw error;
41
+ if (s.tracker && !String(error).includes('READINESS_'))
42
+ s.tracker.unavailable();
43
+ if (s.tracker && isDeviceUnavailable(error))
44
+ throw error;
45
+ if (!s.tracker)
46
+ console.warn('waitForUI: poll error (non-fatal):', error instanceof Error ? error.message : String(error));
47
+ }
48
+ async function poll(s, deps) {
49
+ remainingBudget();
50
+ s.polls++;
51
+ try {
52
+ const started = s.clock.now(), a = s.args;
53
+ const tree = await deps.observe({ platform: a.platform, deviceId: a.deviceId });
54
+ observeReadinessWait(tree, started);
55
+ if (a.resolveTapTarget && tree?.error && /ACTION_TIMEOUT|timed out/i.test(String(tree.error)))
56
+ throw new Error('ACTION_TIMEOUT');
57
+ return inspectObservation(s, tree, deps);
58
+ }
59
+ catch (error) {
60
+ pollError(s, error);
61
+ }
62
+ return null;
63
+ }
64
+ async function runAttempts(s, deps) {
65
+ const a = s.args;
66
+ const interval = Math.max(50, Math.min(a.poll_interval_ms || 300, 2000));
67
+ const maximum = a.retry && a.retry.max_attempts ? Math.max(1, a.retry.max_attempts) : 1;
68
+ const backoff = a.retry && a.retry.backoff_ms ? Math.max(0, a.retry.backoff_ms) : 0;
69
+ while (s.attempts < maximum) {
70
+ s.attempts++;
71
+ const deadline = s.clock.now() + (a.timeout_ms || 0);
72
+ while (s.clock.now() <= deadline || (a.singleAttempt && s.polls === 0)) {
73
+ const result = await poll(s, deps);
74
+ if (result)
75
+ return result;
76
+ if (a.singleAttempt)
77
+ break;
78
+ await delay(s, interval || 50);
79
+ }
80
+ if (s.attempts < maximum) {
81
+ if (backoff > 0)
82
+ await delay(s, backoff);
83
+ continue;
84
+ }
85
+ const note = s.requestedIndex !== null && s.count <= s.requestedIndex
86
+ ? ` requested match.index=${s.requestedIndex} but observed ${s.count} match(es)` : ` observed ${s.count} match(es)`;
87
+ return waitFailure(s, s.tracker ? 'TIMEOUT' : 'ELEMENT_NOT_FOUND', `Condition ${a.condition} not satisfied within timeout;${note}`, waitObserved(s));
88
+ }
89
+ }
90
+ export async function waitForUI(input, deps) {
91
+ const state = createUIWait(input, deps.clock);
92
+ const invalid = validateUIWait(state);
93
+ if (invalid)
94
+ return invalid;
95
+ try {
96
+ return await runAttempts(state, deps);
97
+ }
98
+ catch (error) {
99
+ checkDebuggerGuard();
100
+ if (!state.tracker && error instanceof Error && error.message === 'ACTION_TIMEOUT')
101
+ throw error;
102
+ if (state.tracker && isDeviceUnavailable(error))
103
+ throw error;
104
+ return waitFailure(state, state.tracker && String(error).includes('ACTION_TIMEOUT') ? 'TIMEOUT' : 'INTERNAL_ERROR', state.tracker ? 'Readiness predicate wait did not complete' : error instanceof Error ? error.message : String(error), waitObserved(state, state.tracker ? state.met : false));
105
+ }
106
+ }
@@ -47,57 +47,7 @@ export class AndroidManage {
47
47
  const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
48
48
  let apkToInstall = apkPath;
49
49
  try {
50
- const stat = await fs.stat(apkPath).catch(() => null);
51
- if (stat && stat.isDirectory()) {
52
- const detectedJavaHome = await detectJavaHome().catch(() => undefined);
53
- const env = { ...process.env };
54
- if (detectedJavaHome) {
55
- if (env.JAVA_HOME !== detectedJavaHome) {
56
- env.JAVA_HOME = detectedJavaHome;
57
- env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
58
- console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome);
59
- }
60
- }
61
- try {
62
- delete env.SHELL;
63
- }
64
- catch { }
65
- const gradleArgs = ['assembleDebug'];
66
- if (detectedJavaHome) {
67
- gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
68
- gradleArgs.push('--no-daemon');
69
- env.GRADLE_JAVA_HOME = detectedJavaHome;
70
- }
71
- const wrapperPath = path.join(apkPath, 'gradlew');
72
- const useWrapper = existsSync(wrapperPath);
73
- const execCmd = useWrapper ? wrapperPath : 'gradle';
74
- const spawnOpts = { cwd: apkPath, env };
75
- if (useWrapper) {
76
- await fs.chmod(wrapperPath, 0o755).catch(() => { });
77
- // Run wrapper directly to avoid shell splitting of args
78
- spawnOpts.shell = false;
79
- }
80
- else {
81
- // Execute gradle directly without a shell so paths with spaces are preserved
82
- spawnOpts.shell = false;
83
- }
84
- const proc = spawn(execCmd, gradleArgs, spawnOpts);
85
- let stderr = '';
86
- await new Promise((resolve, reject) => {
87
- proc.stderr?.on('data', d => stderr += d.toString());
88
- proc.on('close', code => {
89
- if (code === 0)
90
- resolve();
91
- else
92
- reject(new Error(stderr || `Gradle build failed with code ${code}`));
93
- });
94
- proc.on('error', err => reject(err));
95
- });
96
- const built = await findApk(apkPath);
97
- if (!built)
98
- throw new Error('Could not locate built APK after running Gradle');
99
- apkToInstall = built;
100
- }
50
+ apkToInstall = await prepareInstallApk(apkPath);
101
51
  try {
102
52
  const res = await spawnAdb(['install', '-r', apkToInstall], deviceId);
103
53
  if (res.code === 0) {
@@ -221,3 +171,57 @@ export class AndroidManage {
221
171
  }
222
172
  }
223
173
  }
174
+ async function prepareInstallApk(apkPath) {
175
+ const stat = await fs.stat(apkPath).catch(() => null);
176
+ if (stat && stat.isDirectory()) {
177
+ const detectedJavaHome = await detectJavaHome().catch(() => undefined);
178
+ const env = { ...process.env };
179
+ if (detectedJavaHome) {
180
+ if (env.JAVA_HOME !== detectedJavaHome) {
181
+ env.JAVA_HOME = detectedJavaHome;
182
+ env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`;
183
+ console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome);
184
+ }
185
+ }
186
+ try {
187
+ delete env.SHELL;
188
+ }
189
+ catch { }
190
+ const gradleArgs = ['assembleDebug'];
191
+ if (detectedJavaHome) {
192
+ gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`);
193
+ gradleArgs.push('--no-daemon');
194
+ env.GRADLE_JAVA_HOME = detectedJavaHome;
195
+ }
196
+ const wrapperPath = path.join(apkPath, 'gradlew');
197
+ const useWrapper = existsSync(wrapperPath);
198
+ const execCmd = useWrapper ? wrapperPath : 'gradle';
199
+ const spawnOpts = { cwd: apkPath, env };
200
+ if (useWrapper) {
201
+ await fs.chmod(wrapperPath, 0o755).catch(() => { });
202
+ // Run wrapper directly to avoid shell splitting of args
203
+ spawnOpts.shell = false;
204
+ }
205
+ else {
206
+ // Execute gradle directly without a shell so paths with spaces are preserved
207
+ spawnOpts.shell = false;
208
+ }
209
+ const proc = spawn(execCmd, gradleArgs, spawnOpts);
210
+ let stderr = '';
211
+ await new Promise((resolve, reject) => {
212
+ proc.stderr?.on('data', d => stderr += d.toString());
213
+ proc.on('close', code => {
214
+ if (code === 0)
215
+ resolve();
216
+ else
217
+ reject(new Error(stderr || `Gradle build failed with code ${code}`));
218
+ });
219
+ proc.on('error', err => reject(err));
220
+ });
221
+ const built = await findApk(apkPath);
222
+ if (!built)
223
+ throw new Error('Could not locate built APK after running Gradle');
224
+ return built;
225
+ }
226
+ return apkPath;
227
+ }
@@ -28,15 +28,15 @@ export async function detectProjectPlatform(projectPath) {
28
28
  try {
29
29
  const full = path.join(dir, e);
30
30
  const st = await fs.stat(full).catch(() => null);
31
- if (st && st.isDirectory()) {
32
- const child = await scan(full, depth - 1);
33
- if (child.ios)
34
- res.ios = true;
35
- if (child.android)
36
- res.android = true;
37
- if (res.ios && res.android)
38
- return res;
39
- }
31
+ if (!st || !st.isDirectory())
32
+ continue;
33
+ const child = await scan(full, depth - 1);
34
+ if (child.ios)
35
+ res.ios = true;
36
+ if (child.android)
37
+ res.android = true;
38
+ if (res.ios && res.android)
39
+ return res;
40
40
  }
41
41
  catch { }
42
42
  }
@@ -146,10 +146,8 @@ export class ToolsManage {
146
146
  if (apk)
147
147
  return { artifactPath: apk, output: out };
148
148
  }
149
- catch (err) {
150
- const stdout = err && err.stdout ? String(err.stdout) : '';
151
- const stderr = err && err.stderr ? String(err.stderr) : '';
152
- throw new Error(`flutter build apk failed: ${stderr || stdout || err.message}`);
149
+ catch (thrown) {
150
+ throw flutterBuildError(thrown, 'apk');
153
151
  }
154
152
  }
155
153
  if (!platform || platform === 'ios') {
@@ -160,10 +158,8 @@ export class ToolsManage {
160
158
  if (app)
161
159
  return { artifactPath: app, output: out };
162
160
  }
163
- catch (err) {
164
- const stdout = err && err.stdout ? String(err.stdout) : '';
165
- const stderr = err && err.stderr ? String(err.stderr) : '';
166
- throw new Error(`flutter build ios failed: ${stderr || stdout || err.message}`);
161
+ catch (thrown) {
162
+ throw flutterBuildError(thrown, 'ios');
167
163
  }
168
164
  }
169
165
  }
@@ -172,20 +168,7 @@ export class ToolsManage {
172
168
  // Preserve error message for diagnostics if needed
173
169
  void e;
174
170
  }
175
- // Fallback: try native subproject builds
176
- if (!platform || platform === 'android') {
177
- const androidDir = path.join(projectPath, 'android');
178
- const android = new AndroidManage();
179
- const artifact = await android.build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug');
180
- return artifact;
181
- }
182
- if (!platform || platform === 'ios') {
183
- const iosDir = path.join(projectPath, 'ios');
184
- const ios = new iOSManage();
185
- const artifact = await ios.build(iosDir);
186
- return artifact;
187
- }
188
- return { error: 'Unable to build flutter project' };
171
+ return buildFlutterNative(projectPath, platform, _forceClean);
189
172
  }
190
173
  static async build_react_native({ projectPath, platform, variant, maxWorkers: _maxWorkers, forceClean: _forceClean }) {
191
174
  // silence unused params
@@ -291,50 +274,8 @@ export class ToolsManage {
291
274
  pushEvent({ type: 'build', status: 'failed', error: 'Both platform and projectType parameters are required.' });
292
275
  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).' } };
293
276
  }
294
- // determine platform if not provided by inspecting path or projectType hint
295
- let chosenPlatform = platform;
296
- try {
297
- if (!chosenPlatform) {
298
- // If caller provided projectType, respect it as a hard override and map to platform
299
- if (projectType) {
300
- if (projectType === 'kmp' || projectType === 'react-native' || projectType === 'flutter') {
301
- chosenPlatform = 'android';
302
- pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing android platform` });
303
- }
304
- else if (projectType === 'native' || projectType === 'ios') {
305
- chosenPlatform = 'ios';
306
- pushEvent({ type: 'build', status: 'info', message: `projectType=${projectType} -> forcing ios platform` });
307
- }
308
- else {
309
- pushEvent({ type: 'build', status: 'failed', error: `Unknown projectType: ${projectType}` });
310
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: `Unknown projectType: ${projectType}` } };
311
- }
312
- }
313
- else {
314
- // If autodetect is disabled, require explicit platform or projectType
315
- if (process.env.MCP_DISABLE_AUTODETECT === '1') {
316
- pushEvent({ type: 'build', status: 'failed', error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType' });
317
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'MCP_DISABLE_AUTODETECT=1 requires explicit platform or projectType (ios|android).' } };
318
- }
319
- const det = await detectProjectPlatform(projectPath);
320
- if (det === 'ios' || det === 'android') {
321
- chosenPlatform = det;
322
- }
323
- else if (det === 'ambiguous') {
324
- pushEvent({ type: 'build', status: 'failed', error: 'Ambiguous project (contains both iOS and Android). Please provide platform: "ios" or "android".' });
325
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Ambiguous project - please provide explicit platform parameter (ios|android).' } };
326
- }
327
- else {
328
- // Unknown project type - do not guess. Request explicit platform.
329
- pushEvent({ type: 'build', status: 'failed', error: 'Unknown project type - unable to autodetect platform. Please provide platform or projectType.' });
330
- return { ndjson: events.join('\n') + '\n', result: { success: false, error: 'Unknown project type - please provide platform or projectType (ios|android).' } };
331
- }
332
- }
333
- }
334
- }
335
- catch {
336
- // detection failed; avoid guessing a platform
337
- }
277
+ // Explicit platform/projectType were required above; no autodetection occurs.
278
+ const chosenPlatform = platform;
338
279
  pushEvent({ type: 'build', status: 'started', platform: chosenPlatform });
339
280
  let buildRes;
340
281
  try {
@@ -376,3 +317,25 @@ export class ToolsManage {
376
317
  return { devices };
377
318
  }
378
319
  }
320
+ async function buildFlutterNative(projectPath, platform, _forceClean) {
321
+ // Fallback: try native subproject builds
322
+ if (!platform || platform === 'android') {
323
+ const androidDir = path.join(projectPath, 'android');
324
+ const android = new AndroidManage();
325
+ const artifact = await android.build(androidDir, _forceClean ? 'clean && assembleDebug' : 'assembleDebug');
326
+ return artifact;
327
+ }
328
+ if (!platform || platform === 'ios') {
329
+ const iosDir = path.join(projectPath, 'ios');
330
+ const ios = new iOSManage();
331
+ const artifact = await ios.build(iosDir);
332
+ return artifact;
333
+ }
334
+ return { error: 'Unable to build flutter project' };
335
+ }
336
+ function flutterBuildError(thrown, kind) {
337
+ const err = thrown;
338
+ const stdout = err && err.stdout ? String(err.stdout) : '';
339
+ const stderr = err && err.stderr ? String(err.stderr) : '';
340
+ return new Error(`flutter build ${kind} failed: ${stderr || stdout || err.message}`);
341
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ import { promises as fs } from "fs";
2
+ import { spawn } from "child_process";
3
+ import { getIOSDeviceMetadata, findAppBundle } from "../../utils/ios/utils.js";
4
+ import path from "path";
5
+ import { findProject } from './project-discovery.js';
6
+ import { detectScheme } from './scheme.js';
7
+ export async function buildIOS(projectPath, optsOrVariant) {
8
+ // Support legacy variant string as second arg
9
+ const opts = typeof optsOrVariant === 'string' ? {} : (optsOrVariant || {});
10
+ const env = { ...process.env, ...(opts.env || {}) };
11
+ try {
12
+ // Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
13
+ const project = await resolveBuildProject(projectPath, opts);
14
+ if ('error' in project)
15
+ return project;
16
+ const { projectRootDir } = project;
17
+ const command = await prepareBuild(projectPath, opts, env, project);
18
+ const failure = await executeBuild(command, projectRootDir, env);
19
+ if (failure)
20
+ return failure;
21
+ // Try to locate built .app. First search project tree, then DerivedData if necessary
22
+ const built = await findAppBundle(projectPath);
23
+ if (built)
24
+ return { artifactPath: built };
25
+ // Fallback: search DerivedData for matching product
26
+ const dd = path.join(process.env.HOME || '', 'Library', 'Developer', 'Xcode', 'DerivedData');
27
+ try {
28
+ const entries = await fs.readdir(dd).catch(() => []);
29
+ for (const e of entries) {
30
+ const candidate = path.join(dd, e);
31
+ const found = await findAppBundle(candidate).catch(() => undefined);
32
+ if (found)
33
+ return { artifactPath: found };
34
+ }
35
+ }
36
+ catch { }
37
+ return { error: 'Could not find .app after build' };
38
+ }
39
+ catch (e) {
40
+ return { error: e instanceof Error ? e.message : String(e) };
41
+ }
42
+ }
43
+ async function resolveBuildProject(projectPath, opts) {
44
+ // Resolve projectPath to an absolute path to avoid cwd-relative resolution issues
45
+ const absProjectPath = path.resolve(projectPath);
46
+ // If caller supplied explicit workspace/project, prefer those and set projectRootDir accordingly
47
+ let projectRootDir = absProjectPath;
48
+ let workspace = opts.workspace;
49
+ let proj = opts.project;
50
+ if (workspace) {
51
+ // normalize workspace path and set root to its parent
52
+ workspace = path.isAbsolute(workspace) ? workspace : path.join(absProjectPath, workspace);
53
+ projectRootDir = path.dirname(workspace);
54
+ workspace = path.basename(workspace);
55
+ }
56
+ else if (proj) {
57
+ proj = path.isAbsolute(proj) ? proj : path.join(absProjectPath, proj);
58
+ projectRootDir = path.dirname(proj);
59
+ proj = path.basename(proj);
60
+ }
61
+ else {
62
+ const projectInfo = await findProject(absProjectPath, 4);
63
+ if (!projectInfo)
64
+ return { error: 'No Xcode project or workspace found' };
65
+ projectRootDir = projectInfo.dir || absProjectPath;
66
+ workspace = projectInfo.workspace;
67
+ proj = projectInfo.proj;
68
+ }
69
+ return { projectRootDir, workspace, proj };
70
+ }
71
+ async function prepareBuild(projectPath, opts, env, project) {
72
+ const { projectRootDir, workspace, proj } = project;
73
+ const destinationUDID = await destination(opts, env);
74
+ // Determine xcode command early so it can be used when detecting schemes
75
+ const xcodeCmd = opts.xcodeCmd || env.XCODEBUILD_PATH || 'xcodebuild';
76
+ // Determine available schemes by querying xcodebuild -list rather than guessing
77
+ // Prepare build flags and paths (support incremental builds)
78
+ let buildArgs;
79
+ let chosenScheme = opts.scheme || null;
80
+ // Derived data and result bundle (agent-configurable)
81
+ const derivedDataPath = opts.derivedDataPath || env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData');
82
+ // Use unique result bundle path by default to avoid collisions
83
+ const resultBundlePath = env.MCP_XCODE_RESULTBUNDLE_PATH || path.join(projectRootDir, 'build', 'xcresults', `ResultBundle-${Date.now()}-${Math.random().toString(36).slice(2)}.xcresult`);
84
+ const xcodeJobs = typeof opts.buildJobs === 'number' ? opts.buildJobs : (parseInt(env.MCP_XCODE_JOBS || '', 10) || 4);
85
+ const forceClean = typeof opts.forceClean === 'boolean' ? opts.forceClean : env.MCP_FORCE_CLEAN === '1';
86
+ // ensure result dirs exist
87
+ await fs.mkdir(path.dirname(resultBundlePath), { recursive: true }).catch(() => { });
88
+ await fs.mkdir(derivedDataPath, { recursive: true }).catch(() => { });
89
+ // remove any pre-existing result bundle path to avoid xcodebuild complaining
90
+ await fs.rm(resultBundlePath, { recursive: true, force: true }).catch(() => { });
91
+ if (workspace) {
92
+ const workspacePath = path.join(projectRootDir, workspace);
93
+ if (!chosenScheme)
94
+ chosenScheme = await detectScheme(projectRootDir, xcodeCmd, workspacePath, undefined, projectRootDir);
95
+ const scheme = chosenScheme || workspace.replace(/\.xcworkspace$/, '');
96
+ buildArgs = ['-workspace', workspacePath, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
97
+ }
98
+ else {
99
+ const projectPathFull = path.join(projectRootDir, proj);
100
+ if (!chosenScheme)
101
+ chosenScheme = await detectScheme(projectRootDir, xcodeCmd, undefined, projectPathFull, projectRootDir);
102
+ const scheme = chosenScheme || proj.replace(/\.xcodeproj$/, '');
103
+ buildArgs = ['-project', projectPathFull, '-scheme', scheme, '-configuration', 'Debug', '-sdk', 'iphonesimulator', 'build'];
104
+ }
105
+ // Insert clean if explicitly requested via env or opts
106
+ if (forceClean) {
107
+ const idx = buildArgs.indexOf('build');
108
+ if (idx >= 0)
109
+ buildArgs.splice(idx, 0, 'clean');
110
+ }
111
+ // If we have a destination UDID, add an explicit destination to avoid xcodebuild picking an ambiguous target
112
+ if (destinationUDID) {
113
+ buildArgs.push('-destination', `platform=iOS Simulator,id=${destinationUDID}`);
114
+ }
115
+ // Add derived data and result bundle for diagnostics and faster incremental builds
116
+ buildArgs.push('-derivedDataPath', derivedDataPath);
117
+ buildArgs.push('-resultBundlePath', resultBundlePath);
118
+ // parallelisation and jobs
119
+ buildArgs.push('-parallelizeTargets');
120
+ buildArgs.push('-jobs', String(xcodeJobs));
121
+ // Prepare results directory for backwards-compatible logs
122
+ const resultsDir = path.join(projectPath, 'build-results');
123
+ // Remove any stale results to avoid xcodebuild complaining about existing result bundles
124
+ await fs.rm(resultsDir, { recursive: true, force: true }).catch(() => { });
125
+ await fs.mkdir(resultsDir, { recursive: true }).catch(() => { });
126
+ return { xcodeCmd, buildArgs, resultsDir };
127
+ }
128
+ async function runAttempt(xcodeCmd, buildArgs, projectRootDir, env, timeoutMs) {
129
+ return new Promise((resolve) => {
130
+ const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir, env });
131
+ let stdout = '';
132
+ let stderr = '';
133
+ proc.stdout?.on('data', d => stdout += d.toString());
134
+ proc.stderr?.on('data', d => stderr += d.toString());
135
+ let killed = false;
136
+ const to = setTimeout(() => {
137
+ killed = true;
138
+ try {
139
+ proc.kill('SIGKILL');
140
+ }
141
+ catch { }
142
+ }, timeoutMs);
143
+ proc.on('close', (code) => {
144
+ clearTimeout(to);
145
+ resolve({ code, stdout, stderr, killedByWatchdog: killed });
146
+ });
147
+ proc.on('error', (err) => {
148
+ clearTimeout(to);
149
+ resolve({ code: null, stdout, stderr: String(err), killedByWatchdog: killed });
150
+ });
151
+ });
152
+ }
153
+ async function executeBuild(command, projectRootDir, env) {
154
+ const { xcodeCmd, buildArgs, resultsDir } = command;
155
+ const XCODEBUILD_TIMEOUT = parseInt(env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000; // default 3 minutes
156
+ const MAX_RETRIES = parseInt(env.MCP_XCODEBUILD_RETRIES || '', 10) || 1;
157
+ const tries = MAX_RETRIES + 1;
158
+ let lastStdout = '';
159
+ let lastStderr = '';
160
+ let lastErr = null;
161
+ for (let attempt = 1; attempt <= tries; attempt++) {
162
+ // Run xcodebuild with a watchdog
163
+ const res = await runAttempt(xcodeCmd, buildArgs, projectRootDir, env, XCODEBUILD_TIMEOUT);
164
+ lastStdout = res.stdout;
165
+ lastStderr = res.stderr;
166
+ if (res.code === 0) {
167
+ // success — clear any previous error and stop retrying
168
+ lastErr = null;
169
+ break;
170
+ }
171
+ // record the failure for reporting
172
+ lastErr = new Error(res.stderr || `xcodebuild failed with code ${res.code}`);
173
+ lastErr.code = res.code;
174
+ lastErr.exitCode = res.code;
175
+ lastErr.killedByWatchdog = !!res.killedByWatchdog;
176
+ // write logs for diagnostics (helpful whether killed or not)
177
+ try {
178
+ await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stdout.log`), res.stdout).catch(() => { });
179
+ await fs.writeFile(path.join(resultsDir, `xcodebuild-${attempt}.stderr.log`), res.stderr).catch(() => { });
180
+ }
181
+ catch { }
182
+ // If killed by watchdog and there are remaining attempts, continue to retry
183
+ if (res.killedByWatchdog && attempt < tries) {
184
+ continue;
185
+ }
186
+ // no more retries or not a watchdog kill — break to report lastErr
187
+ if (attempt >= tries)
188
+ break;
189
+ }
190
+ if (lastErr) {
191
+ // Include diagnostics and result bundle path when available; provide structured info useful for agents
192
+ const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
193
+ const envSnapshot = { PATH: env.PATH };
194
+ return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: lastErr.code || null, invokedCommand, cwd: projectRootDir, envSnapshot } };
195
+ }
196
+ return null;
197
+ }
198
+ async function destination(opts, env) {
199
+ // Determine destination: prefer explicit option, then env var, otherwise use booted simulator UDID
200
+ let destinationUDID = opts.destinationUDID || env.MCP_XCODE_DESTINATION_UDID || env.MCP_XCODE_DESTINATION || '';
201
+ if (!destinationUDID) {
202
+ try {
203
+ const meta = await getIOSDeviceMetadata('booted');
204
+ if (meta && meta.id)
205
+ destinationUDID = meta.id;
206
+ }
207
+ catch { }
208
+ }
209
+ return destinationUDID;
210
+ }
@@ -0,0 +1,29 @@
1
+ import { promises as fs } from "fs";
2
+ import path from "path";
3
+ export async function findProject(root, maxDepth = 4) {
4
+ try {
5
+ const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
6
+ for (const e of ents) {
7
+ // .xcworkspace and .xcodeproj are directories on disk (bundles), not regular files
8
+ if (e.name.endsWith('.xcworkspace'))
9
+ return { dir: root, workspace: e.name };
10
+ if (e.name.endsWith('.xcodeproj'))
11
+ return { dir: root, proj: e.name };
12
+ }
13
+ }
14
+ catch { }
15
+ if (maxDepth <= 0)
16
+ return null;
17
+ try {
18
+ const ents = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
19
+ for (const e of ents) {
20
+ if (e.isDirectory()) {
21
+ const candidate = await findProject(path.join(root, e.name), maxDepth - 1);
22
+ if (candidate)
23
+ return candidate;
24
+ }
25
+ }
26
+ }
27
+ catch { }
28
+ return null;
29
+ }
@@ -0,0 +1,18 @@
1
+ import { spawnSync } from "child_process";
2
+ export async function detectScheme(projectRootDir, xcodeCmdInner, workspacePath, projectPathFull, cwd) {
3
+ try {
4
+ const args = workspacePath ? ['-list', '-workspace', workspacePath] : ['-list', '-project', projectPathFull];
5
+ // Run xcodebuild directly to list schemes
6
+ const res = spawnSync(xcodeCmdInner, args, { cwd: cwd || projectRootDir, encoding: 'utf8', timeout: 20000 });
7
+ const out = res.stdout || '';
8
+ const schemesMatch = out.match(/Schemes:\s*\n([\s\S]*?)(?:\n\n|$)/m);
9
+ if (schemesMatch) {
10
+ const block = schemesMatch[1];
11
+ const schemes = block.split(/\n/).map(s => s.trim()).filter(Boolean);
12
+ if (schemes.length)
13
+ return schemes[0];
14
+ }
15
+ }
16
+ catch { }
17
+ return null;
18
+ }