simulator-use 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -2
- package/THIRD_PARTY_LICENSES +61 -0
- package/bin/simulator-use.js +4 -0
- package/dist/act/diff.js +67 -0
- package/dist/act/resolve.js +49 -0
- package/dist/act/steps.js +188 -0
- package/dist/cli.js +201 -0
- package/dist/cloud-hint.js +14 -0
- package/dist/engine/apps.js +153 -0
- package/dist/engine/axe.js +109 -0
- package/dist/engine/exec.js +37 -0
- package/dist/engine/jobs.js +108 -0
- package/dist/engine/lock.js +68 -0
- package/dist/engine/logs.js +115 -0
- package/dist/engine/simctl.js +156 -0
- package/dist/engine/state.js +63 -0
- package/dist/engine/xcode.js +259 -0
- package/dist/engine/xcresult.js +95 -0
- package/dist/errors.js +11 -0
- package/dist/look/read.js +50 -0
- package/dist/look/snapshot.js +459 -0
- package/dist/mcp.js +104 -0
- package/dist/registry.js +73 -0
- package/dist/settings.js +5 -0
- package/dist/tools/app-control.js +127 -0
- package/dist/tools/app-logs.js +128 -0
- package/dist/tools/app-run.js +97 -0
- package/dist/tools/device-use.js +69 -0
- package/dist/tools/devices.js +27 -0
- package/dist/tools/jobs.js +58 -0
- package/dist/tools/launch-app.js +68 -0
- package/dist/tools/screen-record.js +79 -0
- package/dist/tools/tests-run.js +84 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/ui-act.js +444 -0
- package/dist/tools/ui-look.js +39 -0
- package/package.json +50 -4
- package/skill/SKILL.md +24 -0
- package/vendor/axe/.version +1 -0
- package/vendor/axe/AXe_AXe.bundle/skills/axe/SKILL.md +87 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/FBControlCore +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBAccessibilityTraits.h +23 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchitecture.h +20 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBArchiveOperations.h +136 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBBinaryDescriptor.h +73 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-Swift.h +3232 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore-SwiftImport.h +28 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCore.h +47 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreFrameworkLoader.h +104 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger+OSLog.h +30 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBControlCoreLogger.h +107 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBCrashLog.h +20 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataBuffer.h +75 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBDataConsumer.h +173 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileContainer.h +46 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFileReader.h +25 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFourCharCode.h +30 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture+Sync.h +104 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFuture.h +580 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBFutureContextManager.h +61 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstalledApplication.h +30 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsCommands.h +29 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBInstrumentsOperation.h +83 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBObjCExceptionGuard.h +59 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessBuilder.h +272 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessFetcher.h +128 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessIO.h +126 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessStream.h +175 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBProcessTerminationStrategy.h +25 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBScreenshotCommands.h +15 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSettingsCommands.h +22 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSocketServer.h +63 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBSubprocess.h +131 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStream.h +255 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBVideoStreamConfiguration.h +51 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBXCTraceOperation.h +13 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTarget.h +69 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConfiguration.h +322 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetConstants.h +48 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Headers/FBiOSTargetOperation.h +18 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.abi.json +51890 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.private.swiftinterface +1341 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftinterface +1341 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.abi.json +51890 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.private.swiftinterface +1341 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftinterface +1341 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/FBControlCore.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Modules/module.modulemap +10 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/Resources/Info.plist +50 -0
- package/vendor/axe/Frameworks/FBControlCore.framework/Versions/A/_CodeSignature/CodeResources +492 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/FBDeviceControl +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAFCConnection.h +135 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDServiceConnection.h +239 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDefines.h +256 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDevice.h +105 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMDeviceManager.h +59 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDevice.h +72 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBAMRestorableDeviceManager.h +32 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDevice.h +89 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceCommands.h +147 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl-Swift.h +1500 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControl.h +24 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceControlFrameworkLoader.h +25 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceDebugSymbolsCommands.h +29 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBDeviceManager.h +118 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Headers/FBInstrumentsClient.h +49 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.abi.json +16073 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.private.swiftinterface +398 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftinterface +398 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.abi.json +16073 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +398 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftinterface +398 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/FBDeviceControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Modules/module.modulemap +10 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/Resources/Info.plist +50 -0
- package/vendor/axe/Frameworks/FBDeviceControl.framework/Versions/A/_CodeSignature/CodeResources +324 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/FBSimulatorControl +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBFramebuffer.h +24 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBPeriodicStatsTimer.h +43 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimDeviceWrapper.h +86 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulator.h +160 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorBootConfiguration.h +18 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl-Swift.h +2270 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControl.h +21 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorControlFrameworkLoader.h +45 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Headers/FBSimulatorVideoStream.h +153 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.abi.json +46980 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.private.swiftinterface +1072 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftinterface +1072 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.abi.json +46980 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.private.swiftinterface +1072 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftinterface +1072 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/FBSimulatorControl.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Modules/module.modulemap +10 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/Info.plist +50 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/Resources/README.md +124 -0
- package/vendor/axe/Frameworks/FBSimulatorControl.framework/Versions/A/_CodeSignature/CodeResources +293 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBActivityRecord.h +33 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBAttachment.h +25 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBCodeCoverageConfiguration.h +15 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestBundleConnection.h +37 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestConfiguration.h +74 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerAPIMediator.h +46 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestManagerResultSummary.h +19 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBTestReporterAdapter.h +27 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/FBXCTestConfiguration.h +30 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap-Swift.h +1570 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrap.h +22 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Headers/XCTestBootstrapError.h +25 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/arm64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/Project/x86_64-apple-macos.swiftsourceinfo +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.abi.json +15972 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.private.swiftinterface +397 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftinterface +397 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/arm64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.abi.json +15972 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.private.swiftinterface +397 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftdoc +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftinterface +397 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/XCTestBootstrap.swiftmodule/x86_64-apple-macos.swiftmodule +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Modules/module.modulemap +13 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/Resources/Info.plist +50 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/XCTestBootstrap +0 -0
- package/vendor/axe/Frameworks/XCTestBootstrap.framework/Versions/A/_CodeSignature/CodeResources +303 -0
- package/vendor/axe/axe +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line, built from the same tool list as the MCP server:
|
|
3
|
+
* `simulator-use look --find "Sign In"` calls ui_look with { find: "Sign In" }.
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { persistHeld } from "./engine/state.js";
|
|
9
|
+
import { serve } from "./mcp.js";
|
|
10
|
+
import { callTool, toContent, TOOLS, VERSION } from "./registry.js";
|
|
11
|
+
const flag = (name) => `--${name.replace(/_/g, "-")}`;
|
|
12
|
+
/** Parses `--name value` and `--switch` into arguments the tool's schema accepts. */
|
|
13
|
+
export function parseArgs(tool, argv) {
|
|
14
|
+
const props = tool.inputSchema.properties;
|
|
15
|
+
const args = {};
|
|
16
|
+
const list = Object.entries(props).find(([, p]) => p.type === "array")?.[0];
|
|
17
|
+
const words = [...(tool.positional ?? [])];
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const raw = argv[i];
|
|
20
|
+
if (!raw.startsWith("--")) {
|
|
21
|
+
// Bare words fill the tool's positional arguments in order, then a list argument takes the rest.
|
|
22
|
+
const next = words.find((w) => !(w in args));
|
|
23
|
+
if (next) {
|
|
24
|
+
args[next] = raw;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (list) {
|
|
28
|
+
args[list] = [...(args[list] ?? []), raw];
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
return `Unexpected "${raw}". ${usage(tool)}`;
|
|
32
|
+
}
|
|
33
|
+
const [name, inline] = raw.startsWith("--") ? raw.slice(2).split(/=(.*)/s, 2) : [undefined, undefined];
|
|
34
|
+
const key = name?.replace(/-/g, "_");
|
|
35
|
+
const prop = key ? props[key] : undefined;
|
|
36
|
+
if (!key || !prop || prop.type === "array")
|
|
37
|
+
return `Unknown option ${raw}. ${usage(tool)}`;
|
|
38
|
+
if (prop.type === "boolean") {
|
|
39
|
+
args[key] = inline === undefined ? true : inline !== "false";
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const value = inline ?? argv[++i];
|
|
43
|
+
if (value === undefined)
|
|
44
|
+
return `${flag(key)} needs a value.`;
|
|
45
|
+
args[key] = prop.type === "number" ? Number(value) : value;
|
|
46
|
+
}
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
export function usage(tool) {
|
|
50
|
+
const entries = Object.entries(tool.inputSchema.properties);
|
|
51
|
+
const bare = (tool.positional ?? []).map((key) => ` [${key}]`).join("");
|
|
52
|
+
const options = entries.filter(([key, p]) => p.type !== "array" && !tool.positional?.includes(key)).map(([key, p]) => `${flag(key)}${p.type === "boolean" ? "" : ` <${p.type}>`}`);
|
|
53
|
+
const words = entries.filter(([, p]) => p.type === "array").map(([key]) => ` <${key}...>`).join("");
|
|
54
|
+
return `Usage: simulator-use ${tool.command}${bare}${options.length ? ` [${options.join("] [")}]` : ""}${words}`;
|
|
55
|
+
}
|
|
56
|
+
const quoteWord = (value) => (/^[\w@./:#+=,-]+$/.test(value) ? value : JSON.stringify(value));
|
|
57
|
+
/** A hint's MCP arguments (`{device: "X", crashes: true}`) as the command's words and options. */
|
|
58
|
+
function commandArgs(tool, body) {
|
|
59
|
+
const words = [];
|
|
60
|
+
const positional = [...(tool.positional ?? [])];
|
|
61
|
+
for (const [, key, raw] of body.matchAll(/(\w+):\s*("(?:[^"\\]|\\.)*"|[^,]+)/g)) {
|
|
62
|
+
let value = raw.trim();
|
|
63
|
+
try {
|
|
64
|
+
value = JSON.parse(value);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A bare word stays as it is.
|
|
68
|
+
}
|
|
69
|
+
if (positional[0] === key) {
|
|
70
|
+
positional.shift();
|
|
71
|
+
words.push(quoteWord(String(value)));
|
|
72
|
+
}
|
|
73
|
+
else if (value === true)
|
|
74
|
+
words.push(flag(key));
|
|
75
|
+
else
|
|
76
|
+
words.push(`${flag(key)}${value === false ? "=false" : ` ${quoteWord(String(value))}`}`);
|
|
77
|
+
}
|
|
78
|
+
return words;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* On the command line a hint names commands, not MCP tools: `device_use {device: "X"}` reads `simulator-use use X`,
|
|
82
|
+
* and "pass device" reads "pass --device". Only "Next:" lines change: other lines can hold the app's own text.
|
|
83
|
+
*/
|
|
84
|
+
export function forCommandLine(text) {
|
|
85
|
+
const byName = new Map(TOOLS.map((t) => [t.name, t]));
|
|
86
|
+
const options = new Set(TOOLS.flatMap((t) => Object.keys(t.inputSchema.properties)));
|
|
87
|
+
const tools = new RegExp(`\\b(${TOOLS.map((t) => t.name).join("|")})\\b(?:\\s*\\{([^{}]*)\\})?`, "g");
|
|
88
|
+
return text
|
|
89
|
+
.split("\n")
|
|
90
|
+
.map((line) => line.startsWith("Next:")
|
|
91
|
+
? line
|
|
92
|
+
.replace(tools, (_match, name, body) => {
|
|
93
|
+
const tool = byName.get(name);
|
|
94
|
+
return ["simulator-use", tool.command, ...(body ? commandArgs(tool, body) : [])].join(" ");
|
|
95
|
+
})
|
|
96
|
+
.replace(/\b([Pp]ass) (\w+)\b/g, (match, verb, key) => (options.has(key) ? `${verb} ${flag(key)}` : match))
|
|
97
|
+
: line)
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
/** The first sentence of a description, ending in one period. */
|
|
101
|
+
const summary = (description) => `${description.split(/\.\s/)[0].replace(/\.$/, "")}.`;
|
|
102
|
+
export function help() {
|
|
103
|
+
const width = Math.max(...TOOLS.map((t) => t.command.length), 3);
|
|
104
|
+
return [
|
|
105
|
+
`simulator-use ${VERSION}: give your agent a phone.`,
|
|
106
|
+
"",
|
|
107
|
+
"Commands:",
|
|
108
|
+
...TOOLS.map((t) => ` ${t.command.padEnd(width)} ${summary(t.description)}`),
|
|
109
|
+
` ${"mcp".padEnd(width)} Run the MCP server on stdio.`,
|
|
110
|
+
` ${"call".padEnd(width)} Call a tool by its MCP name with JSON arguments and print one JSON line: simulator-use call ui_look '{"find":"Sign In"}'.`,
|
|
111
|
+
"",
|
|
112
|
+
"Examples:",
|
|
113
|
+
" simulator-use run build, install and launch the Xcode project in this folder",
|
|
114
|
+
" simulator-use look read the screen",
|
|
115
|
+
' simulator-use act "fill #email a@b.co" "tap #signIn" \'wait "Welcome"\'',
|
|
116
|
+
" simulator-use logs --crashes why did it crash",
|
|
117
|
+
"",
|
|
118
|
+
"Run simulator-use <command> --help for its options.",
|
|
119
|
+
].join("\n");
|
|
120
|
+
}
|
|
121
|
+
/** One command's help: what it does, its words and options with their choices. */
|
|
122
|
+
export function commandHelp(tool) {
|
|
123
|
+
const lines = [tool.description, "", usage(tool)];
|
|
124
|
+
for (const [key, p] of Object.entries(tool.inputSchema.properties)) {
|
|
125
|
+
const choices = p.type !== "array" && p.enum ? ` (${p.enum.join(", ")})` : "";
|
|
126
|
+
const name = p.type === "array" ? `<${key}...>` : tool.positional?.includes(key) ? `[${key}] or ${flag(key)}` : flag(key);
|
|
127
|
+
lines.push(` ${name} ${p.description}${choices}`);
|
|
128
|
+
}
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
export async function main(argv) {
|
|
132
|
+
const [command, ...rest] = argv;
|
|
133
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
134
|
+
console.log(help());
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (command === "--version" || command === "-v") {
|
|
138
|
+
console.log(VERSION);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
if (command === "mcp") {
|
|
142
|
+
await serve();
|
|
143
|
+
// The client closed stdin: answer what was in flight, then go, even if a child process still holds a pipe.
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
// From here on each command is its own process: the Simulator chosen with `use` is kept in a file.
|
|
147
|
+
persistHeld();
|
|
148
|
+
if (command === "call")
|
|
149
|
+
return call(rest);
|
|
150
|
+
const tool = TOOLS.find((t) => t.command === command || t.name === command);
|
|
151
|
+
if (!tool) {
|
|
152
|
+
console.error(`Unknown command "${command}".\n\n${help()}`);
|
|
153
|
+
return 2;
|
|
154
|
+
}
|
|
155
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
156
|
+
console.log(commandHelp(tool));
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
const args = parseArgs(tool, rest);
|
|
160
|
+
if (typeof args === "string") {
|
|
161
|
+
console.error(args);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
const out = await callTool(tool.name, args, { waitForJobs: true });
|
|
165
|
+
let text = out.text;
|
|
166
|
+
if (out.image) {
|
|
167
|
+
const file = join(tmpdir(), `simulator-use-${Date.now()}.${out.image.mimeType === "image/png" ? "png" : "jpg"}`);
|
|
168
|
+
await writeFile(file, out.image.data);
|
|
169
|
+
text += `\nscreenshot: ${file}`;
|
|
170
|
+
}
|
|
171
|
+
(out.isError ? console.error : console.log)(forCommandLine(text));
|
|
172
|
+
return out.isError ? 1 : 0;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* `simulator-use call <tool> [json]`: one tool by its MCP name, answered as one line of MCP content
|
|
176
|
+
* ({content, isError}), images as base64, for programs that run simulator-use a call at a time.
|
|
177
|
+
*/
|
|
178
|
+
async function call([name, json, ...extra]) {
|
|
179
|
+
const fail = (text) => {
|
|
180
|
+
console.log(JSON.stringify(toContent({ text, isError: true })));
|
|
181
|
+
return 2;
|
|
182
|
+
};
|
|
183
|
+
if (!name)
|
|
184
|
+
return fail(`Usage: simulator-use call <tool> [json]. Tools: ${TOOLS.map((t) => t.name).join(", ")}.`);
|
|
185
|
+
if (extra.length)
|
|
186
|
+
return fail("simulator-use call takes one JSON object of arguments; quote it as one word.");
|
|
187
|
+
let args = {};
|
|
188
|
+
if (json !== undefined) {
|
|
189
|
+
try {
|
|
190
|
+
args = JSON.parse(json);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
return fail(`The arguments are not JSON: ${error.message}.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
197
|
+
return fail("The arguments must be a JSON object, like {\"find\":\"Sign In\"}.");
|
|
198
|
+
const out = await callTool(name, args, { waitForJobs: true });
|
|
199
|
+
console.log(JSON.stringify(toContent(out)));
|
|
200
|
+
return out.isError ? 1 : 0;
|
|
201
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { setting } from "./settings.js";
|
|
2
|
+
/**
|
|
3
|
+
* Where simulator-use can't run (no Mac, no Xcode, no Simulator runtime), the agent is told where to get a Mac: NoMac,
|
|
4
|
+
* which makes simulator-use, rents agents cloud Macs with Xcode by the minute. Never shown when simulator-use works;
|
|
5
|
+
* SIMULATOR_USE_NO_CLOUD_HINT=1 hides it.
|
|
6
|
+
*/
|
|
7
|
+
export const NOMAC_URL = "https://nomac.app/?utm_source=simulator-use";
|
|
8
|
+
export function cloudHint() {
|
|
9
|
+
if (setting("NO_CLOUD_HINT"))
|
|
10
|
+
return "";
|
|
11
|
+
return `No Mac to run it on? NoMac gives your agent a cloud Mac with Xcode and the iOS Simulator ready, billed by the minute: ${NOMAC_URL}`;
|
|
12
|
+
}
|
|
13
|
+
/** A next step, with the hint after it when it is on. */
|
|
14
|
+
export const withCloudHint = (next) => [next, cloudHint()].filter(Boolean).join(" ");
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ToolError } from "../errors.js";
|
|
5
|
+
import { isOurs, run } from "./exec.js";
|
|
6
|
+
import { deviceDir } from "./state.js";
|
|
7
|
+
const simctl = (args, timeoutMs = 60_000) => run("xcrun", ["simctl", ...args], { timeoutMs });
|
|
8
|
+
/** The value of a key in `simctl appinfo`'s old-style plist output. */
|
|
9
|
+
export function plistValue(text, key) {
|
|
10
|
+
const match = new RegExp(`\\b${key} = "?([^";\\n]+)"?;`).exec(text);
|
|
11
|
+
return match?.[1]?.trim();
|
|
12
|
+
}
|
|
13
|
+
export async function appInfo(udid, bundleId) {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await simctl(["appinfo", udid, bundleId], 15_000);
|
|
16
|
+
if (!stdout.includes("CFBundleIdentifier"))
|
|
17
|
+
return undefined;
|
|
18
|
+
// simctl escapes non-ASCII names (Café as Caf\U00e9): the app's own Info.plist has them as written.
|
|
19
|
+
const path = plistValue(stdout, "Path");
|
|
20
|
+
const fromPlist = async (key) => path ? (await run("plutil", ["-extract", key, "raw", join(path, "Info.plist")], { timeoutMs: 10_000 }).catch(() => undefined))?.stdout.trim() || undefined : undefined;
|
|
21
|
+
const name = (await fromPlist("CFBundleDisplayName")) ?? (await fromPlist("CFBundleName")) ?? plistValue(stdout, "CFBundleDisplayName") ?? plistValue(stdout, "CFBundleName");
|
|
22
|
+
return { executable: (await fromPlist("CFBundleExecutable")) ?? plistValue(stdout, "CFBundleExecutable"), name };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** A bundle id as Apple allows it, so it can name files safely. */
|
|
29
|
+
export const BUNDLE_ID = /^[A-Za-z0-9.-]+$/;
|
|
30
|
+
/** Reads the bundle id from a built .app, so `install` can name what it installed. */
|
|
31
|
+
export async function bundleIdOf(appPath) {
|
|
32
|
+
const plist = join(appPath, "Info.plist");
|
|
33
|
+
if (!existsSync(plist))
|
|
34
|
+
throw new ToolError(`${appPath} is not an .app bundle (no Info.plist).`, "Pass the path to a Simulator build, such as build/Build/Products/Debug-iphonesimulator/MyApp.app.");
|
|
35
|
+
const { stdout } = await run("plutil", ["-extract", "CFBundleIdentifier", "raw", plist], { timeoutMs: 10_000 });
|
|
36
|
+
return stdout.trim();
|
|
37
|
+
}
|
|
38
|
+
export async function install(udid, appPath) {
|
|
39
|
+
try {
|
|
40
|
+
await simctl(["install", udid, appPath], 120_000);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new ToolError(`Could not install ${appPath}: ${error.message}`, "Check it is built for the Simulator (iphonesimulator), not a device.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The file a launched app's print() output goes to, kept while the app runs. */
|
|
47
|
+
export async function consoleFile(udid, bundleId) {
|
|
48
|
+
return join(await deviceDir(udid), `${bundleId}.console.log`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Launches fresh (any running copy is stopped) and keeps the app's print() output for app_logs.
|
|
52
|
+
* simctl's --stdout wrote nothing on iOS 26.5, so a detached `simctl launch --console-pty` holds the app's
|
|
53
|
+
* output instead, writing to a file until the app ends; NSUnbufferedIO is what Xcode sets so lines arrive at once.
|
|
54
|
+
*/
|
|
55
|
+
export async function launch(udid, bundleId) {
|
|
56
|
+
const file = await consoleFile(udid, bundleId);
|
|
57
|
+
const fd = openSync(file, "w");
|
|
58
|
+
const child = spawn("xcrun", ["simctl", "launch", "--console-pty", "--terminate-running-process", udid, bundleId], {
|
|
59
|
+
detached: true,
|
|
60
|
+
stdio: ["ignore", fd, fd],
|
|
61
|
+
env: { ...process.env, SIMCTL_CHILD_NSUnbufferedIO: "YES" },
|
|
62
|
+
});
|
|
63
|
+
closeSync(fd);
|
|
64
|
+
// A launch that fails ends the launcher within moments; a running app keeps it alive.
|
|
65
|
+
const code = await new Promise((resolve) => {
|
|
66
|
+
const timer = setTimeout(() => resolve(undefined), 2_000);
|
|
67
|
+
child.once("exit", (c) => (clearTimeout(timer), resolve(c)));
|
|
68
|
+
child.once("error", () => (clearTimeout(timer), resolve(-1)));
|
|
69
|
+
});
|
|
70
|
+
child.unref();
|
|
71
|
+
if (code !== undefined && code !== 0) {
|
|
72
|
+
const output = readFileSync(file, "utf8").trim();
|
|
73
|
+
if (/not installed|FBSOpenApplicationServiceErrorDomain|Unable to find|could not be found/i.test(output)) {
|
|
74
|
+
throw new ToolError(`${bundleId} is not installed on this Simulator.`, 'Install it first: app_control {action: "install", app: "path/to/App.app"}.');
|
|
75
|
+
}
|
|
76
|
+
throw new ToolError(`Could not launch ${bundleId}: ${output.split("\n").slice(-3).join(" ")}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The app's own system log lines, streamed to a file while simulator-use watches it. */
|
|
80
|
+
export async function streamFile(udid, bundleId) {
|
|
81
|
+
return join(await deviceDir(udid), `${bundleId}.syslog.log`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The system log predicate for what an app itself logged: its own sender or subsystem. Apple's frameworks
|
|
85
|
+
* also log faults from inside the app (on iOS 27, a keyboard queue timeout on every first focus); a grep
|
|
86
|
+
* reaches those, and a crash is in the crash report and the print output.
|
|
87
|
+
*/
|
|
88
|
+
export function ownLines(executable, bundleId) {
|
|
89
|
+
const name = executable.replace(/"/g, "");
|
|
90
|
+
return `process == "${name}" AND (sender == "${name}" OR subsystem BEGINSWITH "${bundleId.replace(/"/g, "")}")`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Streams the app's own log lines to a file from before it launches. \`log show\` is not enough: macOS keeps
|
|
94
|
+
* info lines only in memory, and the Simulator's UIKit noise pushed the sample app's "signed in" line out
|
|
95
|
+
* within 30 s (October 19). One stream per app and device; a new launch replaces the old stream.
|
|
96
|
+
*/
|
|
97
|
+
export async function startLogStream(udid, bundleId, executable) {
|
|
98
|
+
await stopLogStream(udid, bundleId);
|
|
99
|
+
const file = await streamFile(udid, bundleId);
|
|
100
|
+
const fd = openSync(file, "w");
|
|
101
|
+
const child = spawn("xcrun", ["simctl", "spawn", udid, "log", "stream", "--style", "compact", "--level", "info", "--predicate", ownLines(executable, bundleId)], {
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: ["ignore", fd, "ignore"],
|
|
104
|
+
});
|
|
105
|
+
closeSync(fd);
|
|
106
|
+
child.unref();
|
|
107
|
+
if (child.pid)
|
|
108
|
+
writeFileSync(`${file}.pid`, String(child.pid));
|
|
109
|
+
// The stream needs a moment to attach before the app's first lines.
|
|
110
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
111
|
+
}
|
|
112
|
+
export async function stopLogStream(udid, bundleId) {
|
|
113
|
+
const pidFile = `${await streamFile(udid, bundleId)}.pid`;
|
|
114
|
+
const pid = existsSync(pidFile) ? Number(readFileSync(pidFile, "utf8")) : 0;
|
|
115
|
+
if (await isOurs(pid, /simctl spawn .* log stream/)) {
|
|
116
|
+
try {
|
|
117
|
+
process.kill(pid);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Gone between the check and the signal.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rmSync(pidFile, { force: true });
|
|
124
|
+
}
|
|
125
|
+
export async function simctlAction(args, what, timeoutMs = 30_000) {
|
|
126
|
+
try {
|
|
127
|
+
return (await simctl(args, timeoutMs)).stdout.trim();
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw new ToolError(`Could not ${what}: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function pushPayload(udid, bundleId, payload) {
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const child = spawn("xcrun", ["simctl", "push", udid, bundleId, "-"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
136
|
+
let stderr = "";
|
|
137
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
138
|
+
child.on("error", reject);
|
|
139
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new ToolError(`Could not send the push: ${stderr.trim()}`))));
|
|
140
|
+
child.stdin.end(payload);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** Puts text on the Simulator's clipboard, for pasting what the keyboard can't type (é, 日本, emoji). */
|
|
144
|
+
export async function setPasteboard(udid, text) {
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
const child = spawn("xcrun", ["simctl", "pbcopy", udid], { stdio: ["pipe", "ignore", "pipe"] });
|
|
147
|
+
let stderr = "";
|
|
148
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
149
|
+
child.on("error", reject);
|
|
150
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new ToolError(`Could not set the Simulator's clipboard: ${stderr.trim()}`))));
|
|
151
|
+
child.stdin.end(text);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ToolError } from "../errors.js";
|
|
7
|
+
import { run } from "./exec.js";
|
|
8
|
+
import { setting } from "../settings.js";
|
|
9
|
+
/**
|
|
10
|
+
* AXe (MIT, github.com/cameroncooke/AXe) reads the Simulator's accessibility tree and sends touches and keys,
|
|
11
|
+
* with no test runner to build. The package carries a pinned copy in vendor/axe; SIMULATOR_USE_AXE overrides it.
|
|
12
|
+
*/
|
|
13
|
+
export function axePath() {
|
|
14
|
+
const override = setting("AXE");
|
|
15
|
+
if (override)
|
|
16
|
+
return override;
|
|
17
|
+
// src/engine and dist/engine are both two levels below the package root.
|
|
18
|
+
const bundled = fileURLToPath(new URL("../../vendor/axe/axe", import.meta.url));
|
|
19
|
+
return existsSync(bundled) ? bundled : "axe";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* After a Simulator's first boot, its accessibility service takes a while to accept a reader: on a fresh
|
|
23
|
+
* Xcode 27 Mac, describe-ui failed with this for about 75 s, then worked.
|
|
24
|
+
*/
|
|
25
|
+
export class AccessibilityStarting extends ToolError {
|
|
26
|
+
}
|
|
27
|
+
// Also right after a boot: "No translation object returned for simulator" (iPhone 17e, 5 s after booting).
|
|
28
|
+
const STARTING = /remote automation session|No translation object returned/i;
|
|
29
|
+
export async function describeUi(udid) {
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await run(axePath(), ["describe-ui", "--udid", udid], { timeoutMs: 30_000 });
|
|
32
|
+
const parsed = JSON.parse(stdout);
|
|
33
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error.code === "ENOENT")
|
|
37
|
+
throw new ToolError("AXe is missing from this install.", "Reinstall simulator-use, or set SIMULATOR_USE_AXE to an axe binary.");
|
|
38
|
+
if (STARTING.test(error.message)) {
|
|
39
|
+
throw new AccessibilityStarting("The Simulator's accessibility service is still starting; after a first boot this can take a minute or two.", "Call again in 30 seconds.");
|
|
40
|
+
}
|
|
41
|
+
throw new ToolError(`Could not read the screen: ${error.message}`, "Wait a second and call ui_look again; if it keeps failing, check the Simulator is booted with devices.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** A JPEG of the screen sized to points, so a point in the picture is a point to tap (402x874 is about 470 image tokens). */
|
|
45
|
+
export async function screenshot(udid, points) {
|
|
46
|
+
const dir = await mkdtemp(join(tmpdir(), "simulator-use-"));
|
|
47
|
+
try {
|
|
48
|
+
const png = join(dir, "screen.png");
|
|
49
|
+
const jpg = join(dir, "screen.jpg");
|
|
50
|
+
await run(axePath(), ["screenshot", "--output", png, "--udid", udid], { timeoutMs: 30_000 });
|
|
51
|
+
const size = points.width > 0 && points.height > 0 ? ["-z", String(Math.round(points.height)), String(Math.round(points.width))] : [];
|
|
52
|
+
await run("sips", [...size, "-s", "format", "jpeg", "-s", "formatOptions", "70", png, "--out", jpg], { timeoutMs: 30_000 });
|
|
53
|
+
return await readFile(jpg);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new ToolError(`Could not take a screenshot: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await rm(dir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const axe = (udid, args, timeoutMs = 30_000) => run(axePath(), [...args, "--udid", udid], { timeoutMs });
|
|
63
|
+
/** One AXe process for several input steps (tap, type, key, key-combo, swipe, sleep): about 0.2 s cheaper per step. */
|
|
64
|
+
export async function batch(udid, steps) {
|
|
65
|
+
try {
|
|
66
|
+
await axe(udid, ["batch", "--ax-cache", "none", "--type-submission", "composite", ...steps.flatMap((s) => ["--step", s])], 60_000);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new ToolError(`The Simulator did not take the input: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** A drag or swipe on its own: AXe's batch takes swipe but not drag. */
|
|
73
|
+
export async function gesture(udid, args) {
|
|
74
|
+
try {
|
|
75
|
+
await axe(udid, args, 30_000);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new ToolError(`The Simulator did not take the gesture: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** The single element at a point, for reading back a field right after typing (about 0.15 s). */
|
|
82
|
+
export async function elementAt(udid, x, y) {
|
|
83
|
+
try {
|
|
84
|
+
// Normally 0.15 s; on a fresh Xcode 27 VM some hung for 8 to 11 s, so a slow one is abandoned for a full read.
|
|
85
|
+
const { stdout } = await axe(udid, ["describe-ui", "--point", `${Math.round(x)},${Math.round(y)}`], 3_000);
|
|
86
|
+
const parsed = JSON.parse(stdout);
|
|
87
|
+
return Array.isArray(parsed) ? parsed[0] : parsed;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** A step string for AXe batch: text goes in single quotes, with any single quote escaped the shell way. */
|
|
94
|
+
export function typeStep(text) {
|
|
95
|
+
return `type '${text.replace(/'/g, `'\\''`)}'`;
|
|
96
|
+
}
|
|
97
|
+
export async function pressButton(udid, button) {
|
|
98
|
+
await axe(udid, ["button", button]);
|
|
99
|
+
}
|
|
100
|
+
/** Sets a slider by accessibility id or label to a percent; AXe drags it there. */
|
|
101
|
+
export async function setSlider(udid, target, percent) {
|
|
102
|
+
const by = target.id ? ["--id", target.id] : ["--label", target.label ?? ""];
|
|
103
|
+
try {
|
|
104
|
+
await axe(udid, ["slider", ...by, "--value", String(percent)]);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new ToolError(`Could not set the slider: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { setting } from "../settings.js";
|
|
3
|
+
/** Runs a program without a shell. Rejects with its stderr when it fails or runs past the timeout. */
|
|
4
|
+
export function run(file, args, options = {}) {
|
|
5
|
+
const started = Date.now();
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
execFile(file, args, { timeout: options.timeoutMs ?? 30_000, maxBuffer: options.maxBytes ?? 64 * 1024 * 1024, encoding: "utf8" }, (error, stdout, stderr) => {
|
|
8
|
+
// SIMULATOR_USE_DEBUG=1 prints every program run and how long it took, to find where time goes.
|
|
9
|
+
if (setting("DEBUG"))
|
|
10
|
+
process.stderr.write(`simulator-use: ${file.split("/").pop()} ${args.slice(0, 2).join(" ")} ${Date.now() - started} ms${error ? " (failed)" : ""}\n`);
|
|
11
|
+
if (!error)
|
|
12
|
+
return resolve({ stdout, stderr });
|
|
13
|
+
const code = error.code;
|
|
14
|
+
const reason = code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
15
|
+
? "printed more output than can be read"
|
|
16
|
+
: error.killed
|
|
17
|
+
? `timed out after ${Math.round((options.timeoutMs ?? 30_000) / 1000)} s`
|
|
18
|
+
: (stderr || error.message).trim();
|
|
19
|
+
reject(Object.assign(new Error(`${file.split("/").pop()} ${args[0] ?? ""}: ${reason}`), { stderr, code: error.code }));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether a pid still belongs to the process we started: a pid file can outlive its process (the Simulator shut
|
|
25
|
+
* down, the Mac restarted), and the number may by then belong to something else that must not be signalled.
|
|
26
|
+
*/
|
|
27
|
+
export async function isOurs(pid, pattern) {
|
|
28
|
+
if (!pid)
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const { stdout } = await run("ps", ["-o", "command=", "-p", String(pid)], { timeoutMs: 5_000 });
|
|
32
|
+
return pattern.test(stdout);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -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);
|