simtap 0.0.0-stage → 0.6.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 (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -2
  3. package/THIRD_PARTY_LICENSES +61 -0
  4. package/bin/simtap.js +4 -0
  5. package/dist/act/diff.js +67 -0
  6. package/dist/act/resolve.js +49 -0
  7. package/dist/act/steps.js +188 -0
  8. package/dist/cli.js +123 -0
  9. package/dist/engine/apps.js +153 -0
  10. package/dist/engine/axe.js +107 -0
  11. package/dist/engine/exec.js +36 -0
  12. package/dist/engine/jobs.js +95 -0
  13. package/dist/engine/logs.js +115 -0
  14. package/dist/engine/simctl.js +191 -0
  15. package/dist/engine/state.js +30 -0
  16. package/dist/engine/xcode.js +253 -0
  17. package/dist/engine/xcresult.js +95 -0
  18. package/dist/errors.js +11 -0
  19. package/dist/look/read.js +50 -0
  20. package/dist/look/snapshot.js +455 -0
  21. package/dist/mcp.js +108 -0
  22. package/dist/registry.js +65 -0
  23. package/dist/tools/app-control.js +127 -0
  24. package/dist/tools/app-logs.js +128 -0
  25. package/dist/tools/app-run.js +94 -0
  26. package/dist/tools/device-use.js +68 -0
  27. package/dist/tools/devices.js +26 -0
  28. package/dist/tools/jobs.js +55 -0
  29. package/dist/tools/launch-app.js +68 -0
  30. package/dist/tools/screen-record.js +79 -0
  31. package/dist/tools/tests-run.js +81 -0
  32. package/dist/tools/types.js +2 -0
  33. package/dist/tools/ui-act.js +435 -0
  34. package/dist/tools/ui-look.js +40 -0
  35. package/package.json +45 -4
  36. package/skill/SKILL.md +23 -0
  37. package/vendor/axe/.version +1 -0
  38. package/vendor/axe/AXe_AXe.bundle/skills/axe/SKILL.md +87 -0
  39. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/FBControlCore +0 -0
  40. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBAccessibilityTraits.h +23 -0
  41. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchitecture.h +20 -0
  42. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchiveOperations.h +136 -0
  43. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBBinaryDescriptor.h +73 -0
  44. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-Swift.h +3232 -0
  45. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-SwiftImport.h +28 -0
  46. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore.h +47 -0
  47. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreFrameworkLoader.h +104 -0
  48. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger+OSLog.h +30 -0
  49. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger.h +107 -0
  50. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBCrashLog.h +20 -0
  51. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataBuffer.h +75 -0
  52. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataConsumer.h +173 -0
  53. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileContainer.h +46 -0
  54. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileReader.h +25 -0
  55. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFourCharCode.h +30 -0
  56. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture+Sync.h +104 -0
  57. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture.h +580 -0
  58. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFutureContextManager.h +61 -0
  59. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstalledApplication.h +30 -0
  60. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsCommands.h +29 -0
  61. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsOperation.h +83 -0
  62. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBObjCExceptionGuard.h +59 -0
  63. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessBuilder.h +272 -0
  64. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessFetcher.h +128 -0
  65. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessIO.h +126 -0
  66. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessStream.h +175 -0
  67. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessTerminationStrategy.h +25 -0
  68. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBScreenshotCommands.h +15 -0
  69. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSettingsCommands.h +22 -0
  70. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSocketServer.h +63 -0
  71. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSubprocess.h +131 -0
  72. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStream.h +255 -0
  73. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStreamConfiguration.h +51 -0
  74. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBXCTraceOperation.h +13 -0
  75. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTarget.h +69 -0
  76. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConfiguration.h +322 -0
  77. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConstants.h +48 -0
  78. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetOperation.h +18 -0
  79. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  80. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  81. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.abi.json +51890 -0
  82. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.private.swiftinterface +1341 -0
  83. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  84. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftinterface +1341 -0
  85. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  86. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.abi.json +51890 -0
  87. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.private.swiftinterface +1341 -0
  88. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  89. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftinterface +1341 -0
  90. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  91. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/module.modulemap +10 -0
  92. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Resources/Info.plist +50 -0
  93. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/_CodeSignature/CodeResources +492 -0
  94. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/FBDeviceControl +0 -0
  95. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAFCConnection.h +135 -0
  96. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDServiceConnection.h +239 -0
  97. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDefines.h +256 -0
  98. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDevice.h +105 -0
  99. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDeviceManager.h +59 -0
  100. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDevice.h +72 -0
  101. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDeviceManager.h +32 -0
  102. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDevice.h +89 -0
  103. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceCommands.h +147 -0
  104. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl-Swift.h +1500 -0
  105. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl.h +24 -0
  106. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControlFrameworkLoader.h +25 -0
  107. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceDebugSymbolsCommands.h +29 -0
  108. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceManager.h +118 -0
  109. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBInstrumentsClient.h +49 -0
  110. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  111. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  112. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.abi.json +16073 -0
  113. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.private.swiftinterface +398 -0
  114. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  115. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftinterface +398 -0
  116. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  117. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.abi.json +16073 -0
  118. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +398 -0
  119. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  120. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftinterface +398 -0
  121. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  122. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/module.modulemap +10 -0
  123. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Resources/Info.plist +50 -0
  124. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/_CodeSignature/CodeResources +324 -0
  125. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/FBSimulatorControl +0 -0
  126. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBFramebuffer.h +24 -0
  127. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBPeriodicStatsTimer.h +43 -0
  128. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimDeviceWrapper.h +86 -0
  129. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulator.h +160 -0
  130. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorBootConfiguration.h +18 -0
  131. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl-Swift.h +2270 -0
  132. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl.h +21 -0
  133. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControlFrameworkLoader.h +45 -0
  134. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorVideoStream.h +153 -0
  135. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  136. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  137. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.abi.json +46980 -0
  138. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.private.swiftinterface +1072 -0
  139. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  140. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftinterface +1072 -0
  141. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  142. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.abi.json +46980 -0
  143. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +1072 -0
  144. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  145. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftinterface +1072 -0
  146. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  147. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/module.modulemap +10 -0
  148. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/Info.plist +50 -0
  149. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/README.md +124 -0
  150. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/_CodeSignature/CodeResources +293 -0
  151. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBActivityRecord.h +33 -0
  152. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBAttachment.h +25 -0
  153. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBCodeCoverageConfiguration.h +15 -0
  154. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestBundleConnection.h +37 -0
  155. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestConfiguration.h +74 -0
  156. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerAPIMediator.h +46 -0
  157. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerResultSummary.h +19 -0
  158. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestReporterAdapter.h +27 -0
  159. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBXCTestConfiguration.h +30 -0
  160. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap-Swift.h +1570 -0
  161. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap.h +22 -0
  162. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrapError.h +25 -0
  163. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  164. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  165. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.abi.json +15972 -0
  166. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.private.swiftinterface +397 -0
  167. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  168. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftinterface +397 -0
  169. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  170. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.abi.json +15972 -0
  171. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.private.swiftinterface +397 -0
  172. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  173. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftinterface +397 -0
  174. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  175. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/module.modulemap +13 -0
  176. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Resources/Info.plist +50 -0
  177. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/XCTestBootstrap +0 -0
  178. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/_CodeSignature/CodeResources +303 -0
  179. package/vendor/axe/axe +0 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the simtap authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,187 @@
1
- # Temporary Holding Version
1
+ # simtap
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Give your agent a phone. simtap lets Claude Code, Codex, Cursor and any other MCP client read and drive the iOS Simulator on your Mac, with a small set of tools and a compact view of the screen.
4
+
5
+ It builds, runs and tests Xcode projects, reads and uses the screen of any app (SwiftUI, UIKit, React Native), answers iOS's permission prompts, reads the app's logs and crashes, and records video. Tested on Xcode 26.5 and 27 with iOS 26.5 and 27, on iPhone and iPad.
6
+
7
+ ## What the agent sees
8
+
9
+ ```
10
+ Demo · Sign In · iPhone 17 Pro (iOS 26.5)
11
+ textfield "Email" #email @201,194
12
+ securefield "Password" #password @201,246
13
+ switch "Remember me" #remember off @341,298
14
+ button "Sign In" #signIn @201,350
15
+ text "Not signed in" #status @201,402
16
+ tabs: Login (selected) @158,822, Items @244,822
17
+ ```
18
+
19
+ - One line per element: kind, label, `#id` (the app's accessibility identifier), state and `@x,y`, the center in points.
20
+ - Groups, icons inside buttons, scroll bars, the status bar and the keyboard's keys are folded away. The tab bar, scrolling and the keyboard are one line each.
21
+ - Only what can be tapped is listed. An alert, a sheet, a menu or a full-screen view in front hides the screen behind it, and iOS's own permission prompts read as alerts over the app. Rows under the navigation bar, the tab bar or the keyboard are counted as hidden (above, below, to the left or right), not listed.
22
+ - Web views (Capacitor, Expo DOM components), maps, games and iOS's out-of-process pickers have nothing for accessibility to read. They come back as one `unreadable` line with their place, so the agent knows to take a screenshot and tap by point.
23
+ - A screenshot, when asked for, is sized to points, so a point in the picture is a point to tap.
24
+ - The sign-in screen above is about 70 tokens. The full accessibility tree it comes from is about 40,000.
25
+
26
+ ## Using the screen
27
+
28
+ `ui_act` takes steps in the same words `ui_look` prints, runs them in order, and answers with what changed:
29
+
30
+ ```
31
+ ui_act steps: ["fill #email gary@example.com", "fill #password secret1", "tap #signIn", "wait \"Welcome\""]
32
+
33
+ 4 steps done in 4.5 s
34
+ changed: #email "gary@example.com" · #password "•••••••" · #status "Welcome, gary@example.com"
35
+ ```
36
+
37
+ Steps:
38
+
39
+ | Step | Does |
40
+ |---|---|
41
+ | `tap #id`, `tap "label"`, `tap button "label"`, `tap @x,y` | Taps; a switch gets a real touch |
42
+ | `fill #id text` | Focuses the field, clears it, types, and reads the text back |
43
+ | `type text` | Types into the field that has focus |
44
+ | `scroll down`, `scroll right @201,402` | Scrolls the screen, or one carousel |
45
+ | `swipe left`, `swipe left "Row 3"` | Swipes the screen, or one row (to reveal Delete) |
46
+ | `hold "Row 5" [seconds]` | Long press, for context menus |
47
+ | `set #volume 75` | Sets a slider to a percent |
48
+ | `back`, `home`, `key return` | The back button (or edge swipe), the home button, a key |
49
+ | `wait "Welcome" [seconds]`, `gone "Loading"` | Waits for something to appear or go |
50
+
51
+ Every step is checked, so nothing succeeds silently:
52
+ - Typed text is read back from the field. When iOS's autocorrect or smart punctuation changes it (`--` to `—`, `'` to `’`), the text is pasted instead, which iOS takes as is. A field that still shows something else stops the call and says what it shows.
53
+ - Text in any language (Café, 日本語, emoji) can be typed: what a US keyboard lacks goes through the Simulator's clipboard.
54
+ - `fill` refuses anything that isn't a text field, so keys never land in whichever field still has focus.
55
+ - A tap that changes nothing is reported, after waiting 2.5 s for late answers such as a permission prompt. A tap inside a web view says it can't be checked.
56
+ - A target that isn't on screen yet gets two seconds to appear, then fails with the next step: "2 elements are below: scroll down", or, when an alert came up in front, "Answer it first: tap "Cancel" or "Open"".
57
+ - The first failing step stops the rest and returns the screen as it is now.
58
+ - A new screen comes back whole; otherwise only what changed.
59
+
60
+ ## Tools
61
+
62
+ | Tool | CLI | Does |
63
+ |---|---|---|
64
+ | `app_run` | `simtap run` | Builds the Xcode project here, installs and launches it, answers with the first screen; errors as `file:line` |
65
+ | `devices` | `simtap devices` | Lists the Simulators on this Mac with their runtime, UDID and state |
66
+ | `device_use` | `simtap use "iPhone 17 Pro"` | Chooses the Simulator the other tools use, booting it (one boot at a time); dark mode, a clean status bar |
67
+ | `app_control` | `simtap app launch dev.example.app` | `install`, `launch` (answers with the first screen), `terminate`, `uninstall`, `open_url`; `grant`, `revoke` and `reset_permissions`; `location`; `push` |
68
+ | `ui_look` | `simtap look` | Reads the screen; `find` keeps matching lines, `screenshot` adds a picture, `full` saves the whole tree |
69
+ | `ui_act` | `simtap act "tap #signIn" ...` | Does steps in order and answers with what changed |
70
+ | `app_logs` | `simtap logs` | The app's print output and its own system log lines; `grep`, `wait_for`, `crashes` |
71
+ | `tests_run` | `simtap test` | Runs the unit and UI tests; counts, and each failure as `file:line`; "0 tests ran" is an error |
72
+ | `screen_record` | `simtap record start` | Starts or stops a video of the screen; stop answers with the MP4 |
73
+ | `job_status` | `simtap job` | Waits up to 40 s for a long build and answers with its result, or its progress |
74
+ | `job_cancel` | `simtap cancel` | Stops a job |
75
+
76
+ The MCP server and the CLI are built from one list, so they always match. The tool list and instructions stay under 4,000 tokens; a test enforces it.
77
+
78
+ ## Build and run
79
+
80
+ `app_run` does the whole loop in one call: it finds the project (a workspace first, then a project, an XcodeGen spec, or `ios/`), picks the scheme, builds for the Simulator, installs, launches, and answers with the first screen.
81
+
82
+ ```
83
+ Built Demo in 4.2 s, installed dev.simtap.demo.
84
+ Launched Demo in 3.9 s.
85
+ Demo · Sign In · iPhone 17 Pro (iOS 26.5)
86
+ textfield "Email" #email @201,194
87
+ ...
88
+ ```
89
+
90
+ - A failed build answers with the errors only, as `Sources/App.swift:49:37: error: ...`, and the path to the full log.
91
+ - Each folder (each git worktree) builds into its own DerivedData, so rebuilds are incremental and worktrees never share products.
92
+ - An XcodeGen project is regenerated from `project.yml` before each build (skipped when nothing changed), so new files are always in it.
93
+ - The next call in the same folder needs no arguments.
94
+ - An MCP call answers within 40 s; a longer build answers with a job, and `job_status` waits for it. From the CLI, `simtap run` simply waits.
95
+ - Expo, React Native, Flutter and Capacitor builds are not done yet; build them with their own tools, then `app_control install`.
96
+
97
+ ## Tests
98
+
99
+ `tests_run` runs the scheme's tests and reads the answer from Xcode's result bundle, not from the scrolling output:
100
+
101
+ ```
102
+ Tests failed: 1 passed, 2 failed (3 in 35.3 s). Result bundle: .../tests-1792370825323.xcresult
103
+ DemoTests/SignInTests/testRejectsAShortPassword(): Tests/SignInTests.swift:12: XCTAssertEqual failed: ("Welcome, gary@example.com") is not equal to ("Invalid email or password")
104
+ DemoTests/rejectsAnEmailWithoutAt(): Tests/SignInTests.swift:18: Expectation failed: ...
105
+ ```
106
+
107
+ - XCTest and Swift Testing both.
108
+ - Each failing test is named the way `only` takes it, to rerun just that one.
109
+ - "0 tests ran" (no test target, or a filter that matched nothing) is an error, never a pass.
110
+
111
+ ## Logs and crashes
112
+
113
+ `app_logs` shows what the app itself said, not the thousands of lines Apple's frameworks log from inside it:
114
+ - Its `print()` output, kept from the moment `app_control` launches it.
115
+ - Its own system log lines (its subsystem, or logged by its own code), streamed from launch. macOS keeps info lines only in memory and drops them within seconds when the Simulator is busy, so reading the log store afterwards misses them.
116
+ - `grep` also searches Apple's lines, for things like a missing image or a layout warning.
117
+ - `wait_for` waits up to 25 seconds for a line, for example after a tap that starts a network call.
118
+ - `crashes` lists crash reports with the crashing thread down to your code (`ItemsView.body.getter (App.swift:64)`), next to the app's last output, which holds Swift's own message (`App.swift:64: Fatal error: ...`). macOS can take up to a minute to write a report.
119
+
120
+ ## Several agents, one Mac
121
+
122
+ - Each agent runs its own MCP server and holds its own Simulator through `device_use`.
123
+ - Boots never overlap, across every simtap process on the Mac: two Simulators booting at once on an 8 GB Mac pushed it into swap for minutes.
124
+ - `SIMTAP_MAX_BOOTED` caps how many run at once; asking for another shuts down the others first.
125
+ - Calls on one Simulator run one at a time, so two agents' steps never interleave.
126
+
127
+ ## What it can't do yet
128
+
129
+ - Read inside web views, Flutter or game canvases: it says so, and a screenshot plus tap by point works.
130
+ - Real devices, Android, rotating to landscape, pinch and multi-touch.
131
+ - Reading text an app hides from accessibility: some React Native apps set a test id as the label, and the read says when labels look like that.
132
+
133
+ ## Install
134
+
135
+ Needs macOS with Xcode and a Simulator runtime, and Node 20 or newer.
136
+
137
+ ```sh
138
+ # Claude Code
139
+ claude mcp add simtap -- npx -y simtap mcp
140
+
141
+ # Codex
142
+ codex mcp add simtap -- npx -y simtap mcp
143
+ ```
144
+
145
+ Cursor, Windsurf, VS Code and other clients that take a JSON config:
146
+
147
+ ```json
148
+ {
149
+ "mcpServers": {
150
+ "simtap": { "command": "npx", "args": ["-y", "simtap", "mcp"] }
151
+ }
152
+ }
153
+ ```
154
+
155
+ Any other client: run `npx -y simtap mcp` as a stdio server. It speaks MCP 2024-11-05 through 2026-07-28.
156
+
157
+ Agents that work from a shell can use the CLI instead; `skill/SKILL.md` in the package teaches it in under 2 KB.
158
+
159
+ From a shell: `npx simtap look`.
160
+
161
+ ## Privacy
162
+
163
+ simtap sends nothing anywhere. There is no telemetry and no network access at runtime: it runs `xcrun simctl`, the bundled AXe binary and `sips`, all on your Mac.
164
+
165
+ ## How it works
166
+
167
+ simtap reads the Simulator through [AXe](https://github.com/cameroncooke/AXe) (MIT), bundled at a pinned version with its checksum checked at packaging, and through `xcrun simctl`. There is no test runner to build, so the first call is as fast as the rest.
168
+
169
+ ## Develop
170
+
171
+ ```sh
172
+ npm install
173
+ npm run axe # fetch the pinned AXe into vendor/axe
174
+ npm test # node --test, no build needed
175
+ npm run build # dist/ for the npm package
176
+ node bin/simtap.js look
177
+ ```
178
+
179
+ `examples/swiftui-demo` is the sample app the tests' screens come from. `scripts/golden.sh` builds it, runs it on a Simulator and checks 19 golden tasks: build and launch in one call, sign in, toggle, open an item, back and scroll, the app's logs, a crash, a build error and a failing test as `file:line`, a video, and a wrong password that must fail loudly.
180
+
181
+ `examples/edge-lab` is an app with every awkward screen: segments, steppers, sliders, menus, alerts, sheets, full-screen covers, permission prompts, text in any language, autocorrect, late content, toasts, swipe-to-delete, long press, a web view, UIKit and a deep link. `scripts/lab.sh` builds it and runs 26 checks on it.
182
+
183
+ `SIMTAP_DEBUG=1` prints every step and every program run with its time.
184
+
185
+ ## License
186
+
187
+ MIT. AXe and the IDB frameworks it includes are MIT too; see `THIRD_PARTY_LICENSES`.
@@ -0,0 +1,61 @@
1
+ simtap ships a copy of AXe (vendor/axe), which includes frameworks from Facebook IDB.
2
+
3
+ ---------------------------------------------------------------------
4
+
5
+ AXe 1.8.0
6
+ https://github.com/cameroncooke/AXe
7
+
8
+ MIT License
9
+
10
+ Copyright (c) 2025 Cameron Cooke
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ ---------------------------------------------------------------------
31
+
32
+ AXe's own third-party notice:
33
+
34
+ This project depends on third-party open source software.
35
+
36
+ ---------------------------------------------------------------------
37
+
38
+ Facebook IDB
39
+ https://github.com/facebook/idb
40
+
41
+ Copyright (c) Meta Platforms, Inc. and affiliates.
42
+
43
+ Licensed under the MIT License:
44
+
45
+ Permission is hereby granted, free of charge, to any person obtaining a copy
46
+ of this software and associated documentation files (the "Software"), to deal
47
+ in the Software without restriction, including without limitation the rights
48
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
49
+ copies of the Software, and to permit persons to whom the Software is
50
+ furnished to do so, subject to the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be included in all
53
+ copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
60
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
61
+ SOFTWARE.
package/bin/simtap.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../dist/cli.js";
3
+
4
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,67 @@
1
+ import { line, quote, ref, render } from "../look/snapshot.js";
2
+ /**
3
+ * Pairs each element before with itself after: by id, then by kind and label, then a field by its place
4
+ * (a field with no label is named by its text, which is what changed). Ids can appear late: right after a
5
+ * launch the tab buttons had none, a second later they did.
6
+ */
7
+ export function pair(before, after) {
8
+ const left = new Set(before);
9
+ const right = new Set(after);
10
+ const pairs = [];
11
+ const match = (same) => {
12
+ for (const b of right) {
13
+ const a = [...left].find((x) => same(x, b));
14
+ if (!a)
15
+ continue;
16
+ pairs.push([a, b]);
17
+ left.delete(a);
18
+ right.delete(b);
19
+ }
20
+ };
21
+ match((a, b) => Boolean(a.id) && a.id === b.id && a.kind === b.kind);
22
+ match((a, b) => a.kind === b.kind && a.label !== undefined && a.label === b.label);
23
+ match((a, b) => a.kind === b.kind && a.frame.x === b.frame.x && a.frame.y === b.frame.y && a.frame.width === b.frame.width);
24
+ return { pairs, added: [...right], gone: [...left] };
25
+ }
26
+ /** Short form for a changed element: `#status "Welcome, gary@example.com"`. */
27
+ function brief(e) {
28
+ let out = e.id && e.id !== e.label ? ref(e.id) : e.kind;
29
+ if (e.label !== undefined)
30
+ out += ` ${quote(e.label)}`;
31
+ if (e.value)
32
+ out += ` =${quote(e.value)}`;
33
+ if (e.state)
34
+ out += ` ${e.state}`;
35
+ return out;
36
+ }
37
+ /**
38
+ * What an action changed, in as few tokens as possible. A new screen (another title or app, or most lines
39
+ * different) comes back whole; otherwise only the changed, new and gone elements, and the keyboard if it moved.
40
+ */
41
+ export function diff(before, after, device) {
42
+ const all = (s) => [...s.elements, ...s.tabs];
43
+ const { pairs, added, gone } = pair(all(before), all(after));
44
+ // What the agent cares about is what an element says; an id showing up late is not a change.
45
+ const says = (e) => `${e.kind}|${e.label ?? ""}|${e.value ?? ""}|${e.state ?? ""}`;
46
+ const edited = pairs.filter(([a, b]) => says(a) !== says(b)).map(([, b]) => b);
47
+ // The same element somewhere else: the content moved (a scroll, a banner pushing rows down).
48
+ const moved = pairs.filter(([a, b]) => says(a) === says(b) && (a.x !== b.x || a.y !== b.y)).map(([, b]) => b);
49
+ const size = all(after).length;
50
+ const newScreen = before.app !== after.app || before.title !== after.title || added.length + gone.length > Math.max(4, size / 2) || moved.length > 3;
51
+ if (newScreen)
52
+ return `screen:\n${render(after, { device })}`;
53
+ const out = [];
54
+ if (edited.length)
55
+ out.push(`changed: ${edited.map(brief).join(" · ")}`);
56
+ if (added.length)
57
+ out.push(`new: ${added.map(line).join(" · ")}`);
58
+ if (gone.length)
59
+ out.push(`gone: ${gone.map(brief).join(" · ")}`);
60
+ if (moved.length)
61
+ out.push(`moved: ${moved.map(line).join(" · ")}`);
62
+ if (before.scroll !== after.scroll && after.scroll)
63
+ out.push(`scroll: ${after.scroll}`);
64
+ if (before.keyboard !== after.keyboard)
65
+ out.push(after.keyboard ? `keyboard: shown, covering y ${after.keyboardTop ?? "?"} and below` : "keyboard: hidden");
66
+ return out.length ? out.join("\n") : "no visible change";
67
+ }
@@ -0,0 +1,49 @@
1
+ import { line } from "../look/snapshot.js";
2
+ import { describeTarget } from "./steps.js";
3
+ /** Kinds a person taps, preferred over plain text with the same label. */
4
+ const TAPPABLE = new Set(["button", "back", "link", "cell", "tab", "switch", "checkbox", "radio", "textfield", "securefield", "searchfield", "textview", "popup", "menu", "segmented", "picker", "slider", "stepper"]);
5
+ /**
6
+ * Finds a target on the screen: by #id exactly; by label exactly (any case), then by a unique partial match;
7
+ * or a point as given. Ties go to tappable elements; a tie that remains is an error listing the candidates.
8
+ */
9
+ export function resolve(snap, target) {
10
+ if (target.by === "point")
11
+ return { x: Math.round(target.x), y: Math.round(target.y) };
12
+ const all = [...snap.elements, ...snap.tabs];
13
+ let found;
14
+ if (target.by === "id") {
15
+ found = all.filter((e) => e.id === target.id);
16
+ }
17
+ else {
18
+ const want = target.label.toLowerCase();
19
+ const ofKind = target.kind ? all.filter((e) => e.kind === target.kind) : all;
20
+ found = ofKind.filter((e) => e.label?.toLowerCase() === want);
21
+ if (!found.length)
22
+ found = ofKind.filter((e) => e.label?.toLowerCase().includes(want) || e.value?.toLowerCase() === want);
23
+ }
24
+ if (found.length > 1) {
25
+ const tappable = found.filter((e) => TAPPABLE.has(e.kind));
26
+ if (tappable.length)
27
+ found = tappable;
28
+ }
29
+ // One id on several elements (every row of a list): the same point twice is one element, else it's ambiguous.
30
+ if (target.by === "id" && found.length > 1 && found.every((e) => e.x === found[0].x && e.y === found[0].y))
31
+ found = [found[0]];
32
+ if (found.length === 1) {
33
+ const element = found[0];
34
+ return { element, x: element.x, y: element.y };
35
+ }
36
+ if (found.length > 1) {
37
+ return {
38
+ error: `${found.length} elements match ${describeTarget(target)}: ${found.slice(0, 5).map(line).join(" · ")}`,
39
+ next: "Use a #id, a quoted exact label, kind plus label (button \"Save\"), or @x,y.",
40
+ ambiguous: true,
41
+ };
42
+ }
43
+ const hidden = snap.hidden.below + snap.hidden.above;
44
+ return {
45
+ error: `No ${describeTarget(target)} on screen.`,
46
+ next: hidden ? `${snap.hidden.below ? `${snap.hidden.below} elements are below: scroll down` : `${snap.hidden.above} elements are above: scroll up`}, or call ui_look.` : "Call ui_look to see what is on screen.",
47
+ };
48
+ }
49
+ export const isResolved = (r) => !("error" in r);
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The steps ui_act takes, one short line each, in the same words ui_look prints:
3
+ *
4
+ * tap #signIn tap "Sign In" tap button "Sign In" tap @201,350
5
+ * fill #email gary@example.com type hello
6
+ * scroll down swipe left swipe left "Row 3" scroll right @201,402
7
+ * hold "Row 3" hold #photo 2 set #volume 75 back home
8
+ * wait "Welcome" wait #status 10 gone "Loading" key return
9
+ */
10
+ /** HID keycodes AXe sends. */
11
+ export const KEYS = { return: 40, escape: 41, delete: 42, tab: 43, space: 44 };
12
+ export const DEFAULT_WAIT_SECONDS = 5;
13
+ export const MAX_WAIT_SECONDS = 30;
14
+ export const DEFAULT_HOLD_SECONDS = 1;
15
+ export const MAX_HOLD_SECONDS = 10;
16
+ const DIRECTIONS = new Set(["up", "down", "left", "right"]);
17
+ /** Reads a target from the start of `rest`. Returns it and what follows, or an error message. */
18
+ export function parseTarget(rest) {
19
+ const s = rest.trimStart();
20
+ const point = /^@(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)(?=\s|$)/.exec(s);
21
+ if (point)
22
+ return { target: { by: "point", x: Number(point[1]), y: Number(point[2]) }, after: s.slice(point[0].length) };
23
+ if (s.startsWith("#")) {
24
+ if (s[1] === '"') {
25
+ const quoted = readQuoted(s.slice(1));
26
+ if (typeof quoted === "string")
27
+ return quoted;
28
+ return { target: { by: "id", id: quoted.value }, after: quoted.after };
29
+ }
30
+ const id = /^#(\S+)/.exec(s)?.[1];
31
+ if (!id)
32
+ return "Expected an id after #.";
33
+ return { target: { by: "id", id }, after: s.slice(id.length + 1) };
34
+ }
35
+ // `button "Sign In"`: a kind, then a quoted label.
36
+ const kinded = /^([a-z]+)\s+(?=")/.exec(s);
37
+ const kind = kinded?.[1];
38
+ const labelPart = kinded ? s.slice(kinded[0].length) : s;
39
+ if (labelPart.startsWith('"')) {
40
+ const quoted = readQuoted(labelPart);
41
+ if (typeof quoted === "string")
42
+ return quoted;
43
+ return { target: { by: "label", label: quoted.value, ...(kind ? { kind } : {}) }, after: quoted.after };
44
+ }
45
+ const word = /^\S+/.exec(s)?.[0];
46
+ if (!word)
47
+ return "Expected a target: #id, \"label\" or @x,y.";
48
+ return { target: { by: "label", label: word }, after: s.slice(word.length) };
49
+ }
50
+ function readQuoted(s) {
51
+ // A JSON string, so \" and \n work as they do in ui_look's output.
52
+ for (let end = 1; end < s.length; end++) {
53
+ if (s[end] === "\\") {
54
+ end++;
55
+ continue;
56
+ }
57
+ if (s[end] === '"') {
58
+ try {
59
+ return { value: JSON.parse(s.slice(0, end + 1)), after: s.slice(end + 1) };
60
+ }
61
+ catch {
62
+ return `Bad quoted text: ${s.slice(0, end + 1)}`;
63
+ }
64
+ }
65
+ }
66
+ return `Missing closing quote: ${s}`;
67
+ }
68
+ /** Text to type: the rest of the line, or a quoted string to keep spaces at its ends. */
69
+ function readText(rest) {
70
+ const s = rest.replace(/^\s/, "");
71
+ if (s.startsWith('"') && s.endsWith('"') && s.length >= 2) {
72
+ try {
73
+ return JSON.parse(s);
74
+ }
75
+ catch {
76
+ return s;
77
+ }
78
+ }
79
+ return s;
80
+ }
81
+ function seconds(rest, fallback = DEFAULT_WAIT_SECONDS, max = MAX_WAIT_SECONDS) {
82
+ const s = rest.trim();
83
+ if (!s)
84
+ return fallback;
85
+ const n = Number(s.replace(/s$/, ""));
86
+ if (!Number.isFinite(n) || n <= 0)
87
+ return `Expected seconds, got "${s}".`;
88
+ return Math.min(n, max);
89
+ }
90
+ /** A target, then an optional number: `wait Welcome back 10`, `hold "Row 3" 2`. An unquoted label may have spaces. */
91
+ function targetThenNumber(rest) {
92
+ const bare = /^(?![#"@])(.+?)(?:\s+(\d+(?:\.\d+)?)(?:s|%)?)?$/.exec(rest.trim());
93
+ if (bare?.[1])
94
+ return { target: { by: "label", label: bare[1] }, number: bare[2] ?? "" };
95
+ const t = parseTarget(rest);
96
+ if (typeof t === "string")
97
+ return t;
98
+ return { target: t.target, number: t.after.trim().replace(/(s|%)$/, "") };
99
+ }
100
+ export function parseStep(line) {
101
+ const trimmed = line.trim();
102
+ const [, verb = "", rest = ""] = /^(\S+)\s*([\s\S]*)$/.exec(trimmed) ?? [];
103
+ switch (verb.toLowerCase()) {
104
+ case "tap": {
105
+ const t = parseTarget(rest);
106
+ if (typeof t === "string")
107
+ return t;
108
+ if (t.after.trim() && t.target.by === "label" && !rest.trimStart().startsWith('"'))
109
+ return { op: "tap", target: { by: "label", label: rest.trim() } };
110
+ if (t.after.trim())
111
+ return `Unexpected "${t.after.trim()}" after the target.`;
112
+ return { op: "tap", target: t.target };
113
+ }
114
+ case "fill": {
115
+ const t = parseTarget(rest);
116
+ if (typeof t === "string")
117
+ return t;
118
+ if (!t.after.trim())
119
+ return "fill needs a target and the text: fill #email gary@example.com";
120
+ return { op: "fill", target: t.target, text: readText(t.after) };
121
+ }
122
+ case "type":
123
+ if (!rest)
124
+ return "type needs the text: type hello";
125
+ return { op: "type", text: readText(` ${rest}`) };
126
+ case "scroll":
127
+ case "swipe": {
128
+ // `swipe left` sweeps the screen; `swipe left "Row 3"` sweeps that row (to delete it, for example).
129
+ const [, word = "", on = ""] = /^(\S*)\s*([\s\S]*)$/.exec(rest.trim()) ?? [];
130
+ const direction = word.toLowerCase();
131
+ if (!DIRECTIONS.has(direction))
132
+ return `${verb} needs a direction: up, down, left or right.`;
133
+ const op = verb.toLowerCase();
134
+ if (!on.trim())
135
+ return { op, direction: direction };
136
+ const t = parseTarget(on.trim().startsWith('"') || on.trim().startsWith("#") || on.trim().startsWith("@") ? on : `"${on.trim()}"`);
137
+ if (typeof t === "string")
138
+ return t;
139
+ return { op, direction: direction, target: t.target };
140
+ }
141
+ case "wait":
142
+ case "gone":
143
+ case "hold": {
144
+ const t = targetThenNumber(rest);
145
+ if (typeof t === "string")
146
+ return t;
147
+ if (verb.toLowerCase() === "hold") {
148
+ const s = seconds(t.number, DEFAULT_HOLD_SECONDS, MAX_HOLD_SECONDS);
149
+ if (typeof s === "string")
150
+ return s;
151
+ return { op: "hold", target: t.target, seconds: s };
152
+ }
153
+ const s = seconds(t.number);
154
+ if (typeof s === "string")
155
+ return s;
156
+ return { op: verb.toLowerCase(), target: t.target, seconds: s };
157
+ }
158
+ case "set": {
159
+ const t = targetThenNumber(rest);
160
+ if (typeof t === "string")
161
+ return t;
162
+ const percent = Number(t.number);
163
+ if (t.number === "" || !Number.isFinite(percent) || percent < 0 || percent > 100)
164
+ return "set takes a slider and a percent from 0 to 100: set #volume 75";
165
+ return { op: "set", target: t.target, percent };
166
+ }
167
+ case "key": {
168
+ const key = rest.trim().toLowerCase();
169
+ if (!(key in KEYS))
170
+ return `key takes one of: ${Object.keys(KEYS).join(", ")}.`;
171
+ return { op: "key", key: key };
172
+ }
173
+ case "back":
174
+ case "home":
175
+ if (rest.trim())
176
+ return `${verb} takes nothing after it.`;
177
+ return { op: verb.toLowerCase() };
178
+ default:
179
+ return `Unknown step "${verb}". Steps: tap, fill, type, scroll, swipe, hold, set, wait, gone, key, back, home.`;
180
+ }
181
+ }
182
+ export function describeTarget(t) {
183
+ if (t.by === "id")
184
+ return `#${t.id}`;
185
+ if (t.by === "point")
186
+ return `@${t.x},${t.y}`;
187
+ return `${t.kind ? `${t.kind} ` : ""}${JSON.stringify(t.label)}`;
188
+ }