simulator-use 0.0.0-stage → 0.9.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -2
  3. package/THIRD_PARTY_LICENSES +61 -0
  4. package/bin/simulator-use.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 +201 -0
  9. package/dist/cloud-hint.js +14 -0
  10. package/dist/engine/apps.js +174 -0
  11. package/dist/engine/axe.js +164 -0
  12. package/dist/engine/exec.js +37 -0
  13. package/dist/engine/jobs.js +108 -0
  14. package/dist/engine/lock.js +68 -0
  15. package/dist/engine/logs.js +115 -0
  16. package/dist/engine/native.js +210 -0
  17. package/dist/engine/simctl.js +156 -0
  18. package/dist/engine/state.js +63 -0
  19. package/dist/engine/xcode.js +259 -0
  20. package/dist/engine/xcresult.js +98 -0
  21. package/dist/errors.js +11 -0
  22. package/dist/look/read.js +70 -0
  23. package/dist/look/snapshot.js +467 -0
  24. package/dist/mcp.js +104 -0
  25. package/dist/registry.js +73 -0
  26. package/dist/settings.js +5 -0
  27. package/dist/tools/app-control.js +127 -0
  28. package/dist/tools/app-logs.js +128 -0
  29. package/dist/tools/app-run.js +97 -0
  30. package/dist/tools/device-use.js +69 -0
  31. package/dist/tools/devices.js +27 -0
  32. package/dist/tools/jobs.js +58 -0
  33. package/dist/tools/launch-app.js +68 -0
  34. package/dist/tools/screen-record.js +79 -0
  35. package/dist/tools/tests-run.js +86 -0
  36. package/dist/tools/types.js +2 -0
  37. package/dist/tools/ui-act.js +447 -0
  38. package/dist/tools/ui-look.js +39 -0
  39. package/package.json +51 -4
  40. package/skill/SKILL.md +24 -0
  41. package/vendor/axe/.version +1 -0
  42. package/vendor/axe/AXe_AXe.bundle/skills/axe/SKILL.md +87 -0
  43. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/FBControlCore +0 -0
  44. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBAccessibilityTraits.h +23 -0
  45. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchitecture.h +20 -0
  46. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchiveOperations.h +136 -0
  47. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBBinaryDescriptor.h +73 -0
  48. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-Swift.h +3232 -0
  49. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-SwiftImport.h +28 -0
  50. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore.h +47 -0
  51. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreFrameworkLoader.h +104 -0
  52. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger+OSLog.h +30 -0
  53. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger.h +107 -0
  54. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBCrashLog.h +20 -0
  55. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataBuffer.h +75 -0
  56. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataConsumer.h +173 -0
  57. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileContainer.h +46 -0
  58. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileReader.h +25 -0
  59. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFourCharCode.h +30 -0
  60. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture+Sync.h +104 -0
  61. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture.h +580 -0
  62. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFutureContextManager.h +61 -0
  63. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstalledApplication.h +30 -0
  64. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsCommands.h +29 -0
  65. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsOperation.h +83 -0
  66. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBObjCExceptionGuard.h +59 -0
  67. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessBuilder.h +272 -0
  68. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessFetcher.h +128 -0
  69. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessIO.h +126 -0
  70. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessStream.h +175 -0
  71. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessTerminationStrategy.h +25 -0
  72. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBScreenshotCommands.h +15 -0
  73. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSettingsCommands.h +22 -0
  74. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSocketServer.h +63 -0
  75. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSubprocess.h +131 -0
  76. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStream.h +255 -0
  77. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStreamConfiguration.h +51 -0
  78. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBXCTraceOperation.h +13 -0
  79. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTarget.h +69 -0
  80. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConfiguration.h +322 -0
  81. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConstants.h +48 -0
  82. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetOperation.h +18 -0
  83. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  84. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  85. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.abi.json +51890 -0
  86. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.private.swiftinterface +1341 -0
  87. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  88. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftinterface +1341 -0
  89. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  90. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.abi.json +51890 -0
  91. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.private.swiftinterface +1341 -0
  92. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  93. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftinterface +1341 -0
  94. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  95. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/module.modulemap +10 -0
  96. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Resources/Info.plist +50 -0
  97. package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/_CodeSignature/CodeResources +492 -0
  98. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/FBDeviceControl +0 -0
  99. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAFCConnection.h +135 -0
  100. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDServiceConnection.h +239 -0
  101. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDefines.h +256 -0
  102. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDevice.h +105 -0
  103. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDeviceManager.h +59 -0
  104. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDevice.h +72 -0
  105. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDeviceManager.h +32 -0
  106. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDevice.h +89 -0
  107. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceCommands.h +147 -0
  108. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl-Swift.h +1500 -0
  109. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl.h +24 -0
  110. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControlFrameworkLoader.h +25 -0
  111. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceDebugSymbolsCommands.h +29 -0
  112. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceManager.h +118 -0
  113. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBInstrumentsClient.h +49 -0
  114. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  115. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  116. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.abi.json +16073 -0
  117. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.private.swiftinterface +398 -0
  118. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  119. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftinterface +398 -0
  120. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  121. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.abi.json +16073 -0
  122. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +398 -0
  123. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  124. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftinterface +398 -0
  125. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  126. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/module.modulemap +10 -0
  127. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Resources/Info.plist +50 -0
  128. package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/_CodeSignature/CodeResources +324 -0
  129. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/FBSimulatorControl +0 -0
  130. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBFramebuffer.h +24 -0
  131. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBPeriodicStatsTimer.h +43 -0
  132. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimDeviceWrapper.h +86 -0
  133. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulator.h +160 -0
  134. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorBootConfiguration.h +18 -0
  135. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl-Swift.h +2270 -0
  136. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl.h +21 -0
  137. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControlFrameworkLoader.h +45 -0
  138. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorVideoStream.h +153 -0
  139. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  140. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  141. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.abi.json +46980 -0
  142. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.private.swiftinterface +1072 -0
  143. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  144. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftinterface +1072 -0
  145. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  146. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.abi.json +46980 -0
  147. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +1072 -0
  148. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  149. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftinterface +1072 -0
  150. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  151. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/module.modulemap +10 -0
  152. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/Info.plist +50 -0
  153. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/README.md +124 -0
  154. package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/_CodeSignature/CodeResources +293 -0
  155. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBActivityRecord.h +33 -0
  156. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBAttachment.h +25 -0
  157. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBCodeCoverageConfiguration.h +15 -0
  158. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestBundleConnection.h +37 -0
  159. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestConfiguration.h +74 -0
  160. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerAPIMediator.h +46 -0
  161. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerResultSummary.h +19 -0
  162. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestReporterAdapter.h +27 -0
  163. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBXCTestConfiguration.h +30 -0
  164. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap-Swift.h +1570 -0
  165. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap.h +22 -0
  166. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrapError.h +25 -0
  167. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
  168. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
  169. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.abi.json +15972 -0
  170. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.private.swiftinterface +397 -0
  171. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
  172. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftinterface +397 -0
  173. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
  174. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.abi.json +15972 -0
  175. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.private.swiftinterface +397 -0
  176. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
  177. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftinterface +397 -0
  178. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
  179. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/module.modulemap +13 -0
  180. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Resources/Info.plist +50 -0
  181. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/XCTestBootstrap +0 -0
  182. package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/_CodeSignature/CodeResources +303 -0
  183. package/vendor/axe/axe +0 -0
  184. package/vendor/axe/simulator-use-engine +0 -0
  185. package/vendor/axe/simulator-use-pbset +0 -0
@@ -0,0 +1,108 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { tryLock } from "./lock.js";
4
+ import { setting } from "../settings.js";
5
+ const jobs = new Map();
6
+ let latest;
7
+ /** Stops every running job: the server is going away, and a build nobody can follow should not keep compiling. */
8
+ export function cancelAllJobs() {
9
+ for (const job of jobs.values())
10
+ if (!job.result)
11
+ job.cancel?.();
12
+ }
13
+ // Node runs no exit handler when a signal ends the process, so the signals stop the jobs themselves.
14
+ process.on("exit", cancelAllJobs);
15
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
16
+ process.once(signal, () => {
17
+ cancelAllJobs();
18
+ process.exit(128 + { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }[signal]);
19
+ });
20
+ }
21
+ export function startJob(title, work) {
22
+ const id = `job_${randomBytes(4).toString("hex")}`;
23
+ const job = { id, title, started: Date.now(), step: "starting", tail: [] };
24
+ const progress = {
25
+ step: (step) => {
26
+ job.step = step;
27
+ },
28
+ line: (line) => {
29
+ job.tail.push(line);
30
+ if (job.tail.length > 20)
31
+ job.tail.shift();
32
+ },
33
+ // Cleared once the part that can be stopped (the build) is over, so a late cancel doesn't claim it stopped.
34
+ setCancel: (cancel) => {
35
+ job.cancel = cancel
36
+ ? () => {
37
+ job.cancelled = true;
38
+ cancel();
39
+ }
40
+ : undefined;
41
+ },
42
+ setLog: (file) => {
43
+ job.log = file;
44
+ },
45
+ };
46
+ job.done = work(progress)
47
+ .catch((error) => ({ text: error.toText?.() ?? `${title} failed: ${error.message}`, isError: true }))
48
+ .then((result) => {
49
+ // Whatever the stopped work reported (a build "fails" when killed), the truth is it was stopped.
50
+ if (job.cancelled)
51
+ result = { text: `${title} was stopped by job_cancel after ${elapsed(job)}.`, isError: true };
52
+ job.result = result;
53
+ job.cancel = undefined;
54
+ return result;
55
+ });
56
+ jobs.set(id, job);
57
+ latest = id;
58
+ return job;
59
+ }
60
+ export const getJob = (id) => jobs.get(id ?? latest ?? "");
61
+ /** Waits for a job up to `ms`, so a caller can answer with the result when it comes soon, or with progress. */
62
+ export async function waitFor(job, ms) {
63
+ if (job.result)
64
+ return job.result;
65
+ return Promise.race([job.done, delay(ms, undefined, { ref: false })]);
66
+ }
67
+ export const elapsed = (job) => {
68
+ const s = Math.round((Date.now() - job.started) / 1000);
69
+ return s >= 60 ? `${Math.floor(s / 60)} min ${s % 60} s` : `${s} s`;
70
+ };
71
+ /** A call answers within this (40 s); longer work goes on as a job. */
72
+ export const ANSWER_WITHIN_MS = (Number(setting("JOB_AFTER_S")) || 40) * 1000;
73
+ /** The result if it comes within 40 s, else the job to follow; the CLI just waits. */
74
+ export async function answer(job, waitForJobs) {
75
+ if (waitForJobs)
76
+ return job.done;
77
+ const done = await waitFor(job, ANSWER_WITHIN_MS);
78
+ if (done)
79
+ return done;
80
+ return { text: `${job.title} is still ${job.step} after ${elapsed(job)}: job ${job.id}.\nNext: job_status {job_id: "${job.id}"}; it waits up to 40 s for the result.` };
81
+ }
82
+ /** Which running job holds a project: xcodebuild runs in that project's DerivedData, which only one may use. */
83
+ const projects = new Map();
84
+ /**
85
+ * Claims a project for a job, or names what already builds it: a job of this process, or the pid of another
86
+ * simulator-use process (each CLI call is its own). Released when the job ends.
87
+ */
88
+ export async function claimProject(projectPath, job) {
89
+ const holder = projects.get(projectPath);
90
+ if (holder && !holder.result && holder !== job)
91
+ return holder;
92
+ // Claimed here first, so a second call of this process waiting on the lock below names this job.
93
+ projects.set(projectPath, job);
94
+ const taken = await tryLock(`project-${projectPath}`);
95
+ if ("holder" in taken) {
96
+ if (projects.get(projectPath) === job)
97
+ projects.delete(projectPath);
98
+ return taken.holder;
99
+ }
100
+ void job.done.finally(() => {
101
+ if (projects.get(projectPath) === job)
102
+ projects.delete(projectPath);
103
+ void taken.release();
104
+ });
105
+ return undefined;
106
+ }
107
+ /** The job this one is: for work that runs inside it and needs to claim things in its name. */
108
+ export const currentJob = (id) => jobs.get(id);
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { stateDir } from "./state.js";
5
+ const alive = (pid) => {
6
+ try {
7
+ process.kill(pid, 0);
8
+ return true;
9
+ }
10
+ catch (error) {
11
+ return error.code === "EPERM";
12
+ }
13
+ };
14
+ /** A lock's directory: names that are not plain words (a project's path) are hashed. */
15
+ const lockDir = (name) => join(stateDir(), `${/^[\w.-]{1,80}$/.test(name) ? name : createHash("sha1").update(name).digest("hex").slice(0, 16)}.lock.d`);
16
+ /**
17
+ * Takes a lock shared by every simulator-use process on this Mac (each agent, and each CLI call, is its own process).
18
+ * The lock is a directory, made atomically, holding the owner's pid; a lock whose owner died, or that was left
19
+ * without a pid for 60 s, is taken over. Returns its release, or the pid that holds it.
20
+ */
21
+ export async function tryLock(name) {
22
+ await mkdir(stateDir(), { recursive: true });
23
+ const lock = lockDir(name);
24
+ const owner = join(lock, "pid");
25
+ for (;;) {
26
+ try {
27
+ await mkdir(lock);
28
+ await writeFile(owner, String(process.pid));
29
+ return {
30
+ // Only its owner removes it: one taken over from a dead owner belongs to someone else now.
31
+ release: async () => {
32
+ if (Number(await readFile(owner, "utf8").catch(() => "0")) === process.pid)
33
+ await rm(lock, { recursive: true, force: true });
34
+ },
35
+ };
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "EEXIST")
39
+ throw error;
40
+ const holder = Number(await readFile(owner, "utf8").catch(() => "0"));
41
+ const age = Date.now() - ((await stat(lock).catch(() => undefined))?.mtimeMs ?? Date.now());
42
+ // An empty pid is a lock being made right now, unless it has been empty for a minute.
43
+ if ((holder && !alive(holder)) || (!holder && age > 60_000)) {
44
+ await rm(lock, { recursive: true, force: true });
45
+ continue;
46
+ }
47
+ return { holder };
48
+ }
49
+ }
50
+ }
51
+ /** Runs work holding the lock, waiting for it up to waitMs; past that, busy() says why it can't. */
52
+ export async function withLock(name, waitMs, busy, work) {
53
+ const deadline = Date.now() + waitMs;
54
+ for (;;) {
55
+ const taken = await tryLock(name);
56
+ if ("release" in taken) {
57
+ try {
58
+ return await work();
59
+ }
60
+ finally {
61
+ await taken.release();
62
+ }
63
+ }
64
+ if (Date.now() > deadline)
65
+ throw busy(taken.holder);
66
+ await new Promise((r) => setTimeout(r, 250));
67
+ }
68
+ }
@@ -0,0 +1,115 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { ownLines } from "./apps.js";
5
+ import { run } from "./exec.js";
6
+ const LEVELS = { Df: "", I: "info", Db: "debug", E: "error", F: "fault" };
7
+ /** `2026-10-19 01:56:22.114 E Demo[94957:88904] (Sender) [subsystem:category] message`; sender and subsystem are optional. */
8
+ const COMPACT = /^\d{4}-\d\d-\d\d (\d\d:\d\d:\d\d\.\d+) (\w+)\s+\S+?\[\d+:\w+\] (?:\(([^)]+)\) )?(?:\[([^\]]+)\] )?(.*)$/;
9
+ /** Parses `log show --style compact`. Activity lines (type A) are bookkeeping, never kept. */
10
+ export function parseSystemLog(output, appSubsystem) {
11
+ const lines = [];
12
+ for (const raw of output.split("\n")) {
13
+ const m = COMPACT.exec(raw);
14
+ if (!m || m[2] === "A")
15
+ continue;
16
+ const [, time, type, sender, source = "", text = ""] = m;
17
+ const [subsystem = "", category = ""] = source.split(":");
18
+ const tag = subsystem && appSubsystem && subsystem.startsWith(appSubsystem) ? category : source || (sender ? `(${sender})` : "");
19
+ lines.push({ time: time, level: LEVELS[type] ?? type, text: `${tag ? `${tag}: ` : ""}${text}` });
20
+ }
21
+ return lines;
22
+ }
23
+ /**
24
+ * The app's lines from the Simulator's system log. Apple's frameworks log a great deal from inside the app's
25
+ * process (UIKit's event dispatch, several lines a tap; on the Simulator, a haptics error every key press), so
26
+ * by default only what the app itself logged is read, and faults from anywhere; `everything` reads it all.
27
+ */
28
+ export async function systemLog(udid, executable, seconds, bundleId, everything = false) {
29
+ const predicate = everything ? `process == "${executable.replace(/"/g, "")}"` : ownLines(executable, bundleId);
30
+ const { stdout } = await run("xcrun", ["simctl", "spawn", udid, "log", "show", "--style", "compact", "--info", "--last", `${Math.max(1, Math.ceil(seconds))}s`, "--predicate", predicate], {
31
+ timeoutMs: 12_000,
32
+ });
33
+ return parseSystemLog(stdout, bundleId);
34
+ }
35
+ /** Lines from the app's own log stream (startLogStream) that are newer than `since` (ms since 1970). */
36
+ export async function streamedLines(file, since, bundleId) {
37
+ let text;
38
+ try {
39
+ text = await readFile(file, "utf8");
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ const lines = [];
45
+ for (const raw of text.split("\n")) {
46
+ // The stamp is the Mac's local time, as `2026-10-19 02:04:48.818`.
47
+ const stamp = /^(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d\.\d+)/.exec(raw);
48
+ if (!stamp || new Date(`${stamp[1]}T${stamp[2]}`).getTime() < since)
49
+ continue;
50
+ lines.push(...parseSystemLog(raw, bundleId));
51
+ }
52
+ return lines;
53
+ }
54
+ /** The app's print() output, without the Simulator's own start-up noise. */
55
+ export function cleanConsole(text) {
56
+ return text
57
+ .replace(/\r/g, "")
58
+ .split("\n")
59
+ .filter((line) => line.trim() && !/^objc\[\d+\]: Class .* is implemented in both/.test(line));
60
+ }
61
+ /**
62
+ * Crash reports of a Simulator app land on the Mac itself, in ~/Library/Logs/DiagnosticReports, named after
63
+ * the app's executable. An .ips file is one JSON header line, then the report as JSON.
64
+ */
65
+ export async function crashReports(executable, since, limit = 3) {
66
+ const dir = join(homedir(), "Library", "Logs", "DiagnosticReports");
67
+ let names;
68
+ try {
69
+ names = (await readdir(dir)).filter((n) => n.startsWith(`${executable}-`) && n.endsWith(".ips"));
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ const files = await Promise.all(names.map(async (n) => ({ file: join(dir, n), at: (await stat(join(dir, n))).mtimeMs })));
75
+ const recent = files.filter((f) => f.at >= since).sort((a, b) => b.at - a.at).slice(0, limit);
76
+ return Promise.all(recent.map(async (f) => {
77
+ const text = await readFile(f.file, "utf8");
78
+ // When it crashed, not when macOS finished writing the report (up to a minute later).
79
+ const time = /"captureTime"\s*:\s*"([^"]+)"/.exec(text)?.[1] ?? new Date(f.at).toISOString();
80
+ return { file: f.file, time, summary: summarizeIps(text, executable) };
81
+ }));
82
+ }
83
+ /**
84
+ * What went wrong and where: the exception, the reason, and the crashing thread from the top down to just past
85
+ * the app's own last frame (the rest is the framework that called it).
86
+ */
87
+ export function summarizeIps(text, executable) {
88
+ const body = text.slice(text.indexOf("\n") + 1);
89
+ let ips;
90
+ try {
91
+ ips = JSON.parse(body);
92
+ }
93
+ catch {
94
+ return ["(unreadable crash report)"];
95
+ }
96
+ const out = [];
97
+ if (ips.exception)
98
+ out.push(`exception: ${[ips.exception.type, ips.exception.signal].filter(Boolean).join(" ")}`);
99
+ const reasons = [...(ips.termination?.reasons ?? []), ...Object.values(ips.asi ?? {}).flat(), ips.termination?.indicator ?? ""].filter(Boolean);
100
+ for (const reason of reasons.slice(0, 3))
101
+ out.push(`reason: ${reason.replace(/\s+/g, " ").trim()}`);
102
+ const thread = ips.threads?.find((t) => t.triggered) ?? ips.threads?.[ips.faultingThread ?? 0];
103
+ const all = thread?.frames ?? [];
104
+ const imageOf = (f) => ips.usedImages?.[f.imageIndex ?? -1]?.name ?? "?";
105
+ const own = (f) => Boolean(executable) && (imageOf(f) === executable || imageOf(f).startsWith(`${executable}.`));
106
+ const lastOwn = all.reduce((last, f, i) => (own(f) ? i : last), -1);
107
+ const frames = all.slice(0, lastOwn >= 0 ? Math.min(lastOwn + 3, 12) : 8).map((f) => {
108
+ const image = imageOf(f);
109
+ const where = f.sourceFile ? ` (${f.sourceFile}:${f.sourceLine ?? "?"})` : "";
110
+ return ` ${image} ${f.symbol ?? `+${f.imageOffset ?? 0}`}${where}`;
111
+ });
112
+ if (frames.length)
113
+ out.push("crashed thread:", ...frames);
114
+ return out;
115
+ }
@@ -0,0 +1,210 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createInterface } from "node:readline";
6
+ import { fileURLToPath } from "node:url";
7
+ import { setting } from "../settings.js";
8
+ /**
9
+ * The native engine (native/engine): one long-lived process per booted Simulator that keeps CoreSimulator's
10
+ * HID connection, accessibility and screen buffer open, so a tap is about a millisecond and a full read a few
11
+ * hundred, where AXe pays one to three seconds per call to set up. When it is missing or fails, every caller
12
+ * falls back to AXe. It sits beside AXe's frameworks (vendor/axe), which it loads; SIMULATOR_USE_ENGINE points
13
+ * at another build, or "off" turns it off.
14
+ */
15
+ export function enginePath() {
16
+ const override = setting("ENGINE");
17
+ if (override === "off" || override === "0")
18
+ return undefined;
19
+ if (override)
20
+ return override;
21
+ const bundled = fileURLToPath(new URL("../../vendor/axe/simulator-use-engine", import.meta.url));
22
+ return existsSync(bundled) ? bundled : undefined;
23
+ }
24
+ const START_MS = 15_000;
25
+ /**
26
+ * An engine that answers nothing for this long is hung, and is ended. A request merely slower than its caller's
27
+ * wait never ends it: a new engine's first read costs 1 to 5 s and its cached web page is lost, and on a phone
28
+ * copied from another user's golden a new engine could not open an automation session at all (a web field's first
29
+ * read took 4.2 s against a 3 s wait, and ending the engine then left that phone unreadable).
30
+ */
31
+ const HUNG_MS = 120_000;
32
+ class Engine {
33
+ child;
34
+ lines;
35
+ queue = Promise.resolve();
36
+ dead = false;
37
+ ready;
38
+ constructor(path, udid) {
39
+ this.child = spawn(path, ["--udid", udid], { stdio: ["pipe", "pipe", "ignore"] });
40
+ // The engine closes its automation session when its input ends, so a CLI process may exit at any time:
41
+ // nothing here keeps Node alive, and the engine finishes on its own.
42
+ this.child.unref();
43
+ this.child.stdin?.unref?.();
44
+ this.child.stdout?.unref?.();
45
+ this.child.on("exit", () => (this.dead = true));
46
+ this.child.on("error", () => (this.dead = true));
47
+ this.lines = createInterface({ input: this.child.stdout })[Symbol.asyncIterator]();
48
+ this.ready = this.next(START_MS).then((line) => {
49
+ const ready = parse(line);
50
+ if (!ready?.ready)
51
+ this.kill();
52
+ return Boolean(ready?.ready);
53
+ }, () => (this.kill(), false));
54
+ }
55
+ async next(timeoutMs) {
56
+ let timer;
57
+ try {
58
+ const line = await Promise.race([
59
+ this.lines.next(),
60
+ new Promise((_, reject) => (timer = setTimeout(() => reject(new Error("the engine did not answer in time")), timeoutMs))),
61
+ ]);
62
+ if (line.done)
63
+ throw new Error("the engine stopped");
64
+ return line.value;
65
+ }
66
+ finally {
67
+ clearTimeout(timer);
68
+ }
69
+ }
70
+ /**
71
+ * One request at a time, in order. A caller waits timeoutMs and then gets a busy answer, while the engine
72
+ * finishes: the next request still waits for this one's answer, which is read and dropped, so answers stay paired
73
+ * with their requests and the engine stays.
74
+ */
75
+ call(request, timeoutMs) {
76
+ const answer = this.queue.then(async () => {
77
+ if (this.dead)
78
+ throw new Error("the engine stopped");
79
+ this.child.stdin.write(`${JSON.stringify(request)}\n`);
80
+ try {
81
+ return parse(await this.next(HUNG_MS)) ?? { ok: false, error: "unreadable answer" };
82
+ }
83
+ catch (error) {
84
+ this.kill();
85
+ throw error;
86
+ }
87
+ });
88
+ this.queue = answer.catch(() => undefined);
89
+ let timer;
90
+ const late = new Promise((resolve) => {
91
+ timer = setTimeout(() => resolve({ ok: false, busy: true, error: `no answer within ${timeoutMs} ms` }), timeoutMs);
92
+ timer.unref();
93
+ });
94
+ return Promise.race([answer, late]).finally(() => clearTimeout(timer));
95
+ }
96
+ kill() {
97
+ this.dead = true;
98
+ this.child.stdin?.end();
99
+ setTimeout(() => this.child.kill(), 2_000).unref();
100
+ }
101
+ }
102
+ function parse(line) {
103
+ try {
104
+ return JSON.parse(line);
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ const engines = new Map();
111
+ /** Phones whose engine would not start: AXe for the rest of this process. */
112
+ const unavailable = new Set();
113
+ async function engineFor(udid) {
114
+ const path = enginePath();
115
+ if (!path || unavailable.has(udid))
116
+ return undefined;
117
+ let engine = engines.get(udid);
118
+ if (!engine || engine.dead) {
119
+ engine = new Engine(path, udid);
120
+ engines.set(udid, engine);
121
+ }
122
+ if (await engine.ready)
123
+ return engine;
124
+ engines.delete(udid);
125
+ unavailable.add(udid);
126
+ return undefined;
127
+ }
128
+ /**
129
+ * A request to the phone's engine, or undefined when there is none (the caller uses AXe). An answer with busy set
130
+ * means the engine is still working on it: the caller goes on without it, never with AXe, whose own automation
131
+ * session would compete with the engine's.
132
+ */
133
+ export async function engineCall(udid, request, timeoutMs = 30_000) {
134
+ const engine = await engineFor(udid);
135
+ if (!engine)
136
+ return undefined;
137
+ try {
138
+ return await engine.call(request, timeoutMs);
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ /**
145
+ * Each AXe read opens and closes an automation session of its own. A phone refuses a session opened too soon after
146
+ * the last one closed, and then every session after it (measured on iOS 27), so the time goes where the next engine
147
+ * looks before opening one (native/engine: takeSessionTurn).
148
+ */
149
+ export function noteSessionClosed(udid) {
150
+ try {
151
+ writeFileSync(join(sessionFolder(), `simulator-use-engine-${udid}.lock`), String(Date.now() / 1000), { mode: 0o600 });
152
+ }
153
+ catch {
154
+ // Best effort: the engine still waits for its own turn.
155
+ }
156
+ }
157
+ let folder;
158
+ /** Where the engine keeps its lock: $TMPDIR, else the user's own temporary folder (as NSTemporaryDirectory()). */
159
+ function sessionFolder() {
160
+ folder ??= process.env.TMPDIR || execFileSync("getconf", ["DARWIN_USER_TEMP_DIR"], { encoding: "utf8" }).trim() || tmpdir();
161
+ return folder;
162
+ }
163
+ /** Ends this process's engines (tests, and a phone that was shut down). */
164
+ export function closeEngines(udid) {
165
+ for (const [id, engine] of engines)
166
+ if (!udid || id === udid)
167
+ (engine.kill(), engines.delete(id));
168
+ }
169
+ /**
170
+ * AXe's batch steps, as the engine's steps. Undefined for any step it does not know, so the whole batch goes to
171
+ * AXe as before.
172
+ */
173
+ export function engineSteps(steps) {
174
+ const out = [];
175
+ for (const step of steps) {
176
+ let m;
177
+ if ((m = step.match(/^tap -x (-?[\d.]+) -y (-?[\d.]+)( --tap-style physical)?$/))) {
178
+ // Switches and checkboxes need a real touch down and up (see tapStep in ui-act).
179
+ out.push(m[3] ? { touch: [Number(m[1]), Number(m[2])], hold: 0.1 } : { tap: [Number(m[1]), Number(m[2])] });
180
+ }
181
+ else if ((m = step.match(/^sleep ([\d.]+)$/)))
182
+ out.push({ sleep: Number(m[1]) });
183
+ else if ((m = step.match(/^key-combo --modifiers ([\d,]+) --key (\d+)$/)))
184
+ out.push({ combo: { mods: m[1].split(",").map(Number), key: Number(m[2]) } });
185
+ else if ((m = step.match(/^key (\d+)$/)))
186
+ out.push({ key: Number(m[1]) });
187
+ else if ((m = step.match(/^type '([\s\S]*)'$/)))
188
+ out.push({ type: m[1].replace(/'\\''/g, "'") });
189
+ else if ((m = step.match(/^touch -x (-?[\d.]+) -y (-?[\d.]+) --down --up --delay ([\d.]+)$/)))
190
+ out.push({ touch: [Number(m[1]), Number(m[2])], hold: Number(m[3]) });
191
+ else
192
+ return undefined;
193
+ }
194
+ return out;
195
+ }
196
+ /** AXe's swipe and drag arguments, as an engine request; undefined for anything else. */
197
+ export function engineGesture(args) {
198
+ const [kind, ...rest] = args;
199
+ if (kind !== "swipe" && kind !== "drag")
200
+ return undefined;
201
+ const value = (flag) => {
202
+ const i = rest.indexOf(flag);
203
+ return i >= 0 ? Number(rest[i + 1]) : NaN;
204
+ };
205
+ const [x1, y1, x2, y2] = ["--start-x", "--start-y", "--end-x", "--end-y"].map(value);
206
+ const duration = value("--duration");
207
+ if ([x1, y1, x2, y2].some((n) => !Number.isFinite(n)))
208
+ return undefined;
209
+ return { op: kind, x1, y1, x2, y2, duration: Number.isFinite(duration) ? duration : kind === "drag" ? 0.6 : 0.3 };
210
+ }
@@ -0,0 +1,156 @@
1
+ import { withCloudHint } from "../cloud-hint.js";
2
+ import { ToolError } from "../errors.js";
3
+ import { run } from "./exec.js";
4
+ import { withLock } from "./lock.js";
5
+ const NO_SIMULATOR = "This computer can't run the iOS Simulator: it needs macOS with Xcode and a Simulator runtime.";
6
+ /** "com.apple.CoreSimulator.SimRuntime.iOS-26-5" becomes "iOS 26.5". */
7
+ export function runtimeName(identifier) {
8
+ const match = /SimRuntime\.([A-Za-z]+)-(\d+)-(\d+)(?:-(\d+))?$/.exec(identifier);
9
+ if (!match)
10
+ return identifier;
11
+ const [, os, major, minor, patch] = match;
12
+ return `${os === "xrOS" ? "visionOS" : os} ${major}.${minor}${patch ? `.${patch}` : ""}`;
13
+ }
14
+ /** Parses `simctl list devices --json`: available devices only, booted first, then by runtime and name. */
15
+ export function parseDevices(json) {
16
+ const parsed = JSON.parse(json);
17
+ const devices = [];
18
+ for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
19
+ for (const d of list) {
20
+ if (d.isAvailable === false)
21
+ continue;
22
+ devices.push({ udid: d.udid, name: d.name, state: d.state, runtime: runtimeName(runtime), booted: d.state === "Booted" });
23
+ }
24
+ }
25
+ // iOS before watchOS, tvOS and visionOS (by name, "watchOS 26" would sort first); the newest iOS first;
26
+ // iPhones before iPads: most agents want a phone.
27
+ const platform = (d) => (d.runtime.startsWith("iOS ") ? 0 : 1);
28
+ const version = (d) => d.runtime.replace(/^\S+\s/, "");
29
+ const family = (d) => ["iPhone", "iPad"].findIndex((p) => d.name.startsWith(p)) >>> 0;
30
+ return devices.sort((a, b) => Number(b.booted) - Number(a.booted) ||
31
+ platform(a) - platform(b) ||
32
+ version(b).localeCompare(version(a), undefined, { numeric: true }) ||
33
+ family(a) - family(b) ||
34
+ a.name.localeCompare(b.name, undefined, { numeric: true }));
35
+ }
36
+ /** The MCP server lives for a whole session: one list serves the calls of the next few seconds (0.35 s saved each). */
37
+ const CACHE_MS = 5_000;
38
+ let cached;
39
+ export async function listDevices(fresh = false) {
40
+ if (process.platform !== "darwin")
41
+ throw new ToolError(NO_SIMULATOR, withCloudHint("Run simulator-use on a Mac with Xcode."));
42
+ if (!fresh && cached && Date.now() - cached.at < CACHE_MS)
43
+ return cached.devices;
44
+ try {
45
+ const { stdout } = await run("xcrun", ["simctl", "list", "devices", "--json"], { timeoutMs: 30_000 });
46
+ cached = { at: Date.now(), devices: parseDevices(stdout) };
47
+ return cached.devices;
48
+ }
49
+ catch (error) {
50
+ throw listError(error);
51
+ }
52
+ }
53
+ /**
54
+ * Why simctl gave no device list. A timeout means the Simulator service is still starting (it can take a minute
55
+ * the first time after a reboot or an Xcode update), not that this computer can't run it.
56
+ */
57
+ export function listError(error) {
58
+ if (/timed out after/.test(error.message)) {
59
+ return new ToolError("The Simulator service did not answer within 30 s: it can take a minute to start the first time after a reboot or an Xcode update.", "Call again in 30 s.");
60
+ }
61
+ return new ToolError(`${NO_SIMULATOR} (${error.message})`, withCloudHint("Install Xcode from the App Store, open it once, then add an iOS Simulator runtime in Settings > Components."));
62
+ }
63
+ export async function xcodeVersion() {
64
+ try {
65
+ const { stdout } = await run("xcodebuild", ["-version"], { timeoutMs: 15_000 });
66
+ return stdout.split("\n")[0]?.trim();
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ /** A device by name or UDID; a name on several runtimes means the booted one, else the newest. */
73
+ export function findDevice(devices, wanted) {
74
+ const want = wanted.trim().toLowerCase();
75
+ const matches = devices.filter((d) => d.udid.toLowerCase() === want || d.name.toLowerCase() === want);
76
+ return matches.find((d) => d.booted) ?? matches[0];
77
+ }
78
+ /**
79
+ * The device a call works on: the one named (by name or UDID), else the one this agent holds (device_use),
80
+ * else the only booted one.
81
+ */
82
+ export function pickDevice(devices, wanted, held) {
83
+ const booted = devices.filter((d) => d.booted);
84
+ if (wanted) {
85
+ const device = findDevice(devices, wanted);
86
+ if (!device)
87
+ throw new ToolError(`No Simulator named "${wanted}".`, "Call devices to see the names and UDIDs on this Mac.");
88
+ if (!device.booted)
89
+ throw new ToolError(`${device.name} (${device.runtime}) is not booted.`, `Call device_use {device: "${device.udid}"} to boot it.`);
90
+ return device;
91
+ }
92
+ const mine = held && booted.find((d) => d.udid === held);
93
+ if (mine)
94
+ return mine;
95
+ // The device this agent chose was shut down (by another agent's boot, say): never slide onto someone else's.
96
+ const gone = held && devices.find((d) => d.udid === held);
97
+ if (gone)
98
+ throw new ToolError(`${gone.name} (${gone.runtime}), the Simulator this agent was using, is no longer booted.`, `Call device_use {device: "${gone.udid}"} to boot it again, or device_use another.`);
99
+ if (booted.length === 1)
100
+ return booted[0];
101
+ if (!booted.length) {
102
+ const suggestion = devices.find((d) => d.name.startsWith("iPhone")) ?? devices[0];
103
+ throw new ToolError("No Simulator is booted.", suggestion ? `Call device_use {device: "${suggestion.name}"} to boot ${suggestion.name} (${suggestion.runtime}).` : "Call devices to see what this Mac has.");
104
+ }
105
+ throw new ToolError(`${booted.length} Simulators are booted: ${booted.map((d) => `${d.name} (${d.udid})`).join(", ")}.`, "Call device_use with the one to use, or pass device.");
106
+ }
107
+ /** A call may take 45 s: waiting for another boot and this boot share 40 of them. */
108
+ const BOOT_BUDGET_MS = 40_000;
109
+ const LOCK_WAIT_MS = 20_000;
110
+ /**
111
+ * One boot at a time on this Mac, across every simulator-use process (each agent runs its own), held in a lock
112
+ * (lock.ts) that a dead owner's successor takes over.
113
+ */
114
+ const withBootLock = (work) => withLock("boot", LOCK_WAIT_MS, () => new ToolError("Another Simulator is still booting, and boots never overlap.", "Call device_use again in 30 s."), work);
115
+ /**
116
+ * Boots a device and waits until it is ready, within the time a call may take. Two Simulators booting at once
117
+ * on an 8 GB Mac pushed it into swap for minutes (measured October 18), so boots never overlap. With a limit on
118
+ * booted devices (SIMULATOR_USE_MAX_BOOTED), others are shut down first, inside the lock so two calls can't both
119
+ * see room.
120
+ */
121
+ export async function bootDevice(device, maxBooted = Infinity) {
122
+ const asked = Date.now();
123
+ return withBootLock(async () => {
124
+ const started = Date.now();
125
+ // A boot started outside simulator-use (Xcode, simctl) is waited out too.
126
+ while (Date.now() - asked < LOCK_WAIT_MS && (await listDevices(true)).some((d) => d.state === "Booting" && d.udid !== device.udid)) {
127
+ await new Promise((r) => setTimeout(r, 1_000));
128
+ }
129
+ const others = (await listDevices(true)).filter((d) => d.booted && d.udid !== device.udid);
130
+ const shutDown = [];
131
+ for (const d of others.slice(0, Math.max(0, others.length - (maxBooted - 1)))) {
132
+ await shutdownDevice(d);
133
+ shutDown.push(d.name);
134
+ }
135
+ try {
136
+ await run("xcrun", ["simctl", "boot", device.udid], { timeoutMs: 20_000 });
137
+ }
138
+ catch (error) {
139
+ if (!/current state: Booted/i.test(error.message))
140
+ throw new ToolError(`Could not boot ${device.name}: ${error.message}`);
141
+ }
142
+ let ready = true;
143
+ try {
144
+ await run("xcrun", ["simctl", "bootstatus", device.udid, "-b"], { timeoutMs: Math.max(5_000, BOOT_BUDGET_MS - (Date.now() - asked)) });
145
+ }
146
+ catch {
147
+ ready = false;
148
+ }
149
+ cached = undefined;
150
+ return { ms: Date.now() - started, ready, waitedMs: started - asked, shutDown };
151
+ });
152
+ }
153
+ export async function shutdownDevice(device) {
154
+ await run("xcrun", ["simctl", "shutdown", device.udid], { timeoutMs: 15_000 }).catch(() => undefined);
155
+ cached = undefined;
156
+ }