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.
- package/LICENSE +21 -0
- package/README.md +210 -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 +174 -0
- package/dist/engine/axe.js +164 -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/native.js +210 -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 +98 -0
- package/dist/errors.js +11 -0
- package/dist/look/read.js +70 -0
- package/dist/look/snapshot.js +467 -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 +86 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/ui-act.js +447 -0
- package/dist/tools/ui-look.js +39 -0
- package/package.json +51 -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/vendor/axe/simulator-use-engine +0 -0
- package/vendor/axe/simulator-use-pbset +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,174 @@
|
|
|
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 { fileURLToPath } from "node:url";
|
|
5
|
+
import { ToolError } from "../errors.js";
|
|
6
|
+
import { isOurs, run } from "./exec.js";
|
|
7
|
+
import { deviceDir } from "./state.js";
|
|
8
|
+
const simctl = (args, timeoutMs = 60_000) => run("xcrun", ["simctl", ...args], { timeoutMs });
|
|
9
|
+
/** The value of a key in `simctl appinfo`'s old-style plist output. */
|
|
10
|
+
export function plistValue(text, key) {
|
|
11
|
+
const match = new RegExp(`\\b${key} = "?([^";\\n]+)"?;`).exec(text);
|
|
12
|
+
return match?.[1]?.trim();
|
|
13
|
+
}
|
|
14
|
+
export async function appInfo(udid, bundleId) {
|
|
15
|
+
try {
|
|
16
|
+
const { stdout } = await simctl(["appinfo", udid, bundleId], 15_000);
|
|
17
|
+
if (!stdout.includes("CFBundleIdentifier"))
|
|
18
|
+
return undefined;
|
|
19
|
+
// simctl escapes non-ASCII names (Café as Caf\U00e9): the app's own Info.plist has them as written.
|
|
20
|
+
const path = plistValue(stdout, "Path");
|
|
21
|
+
const fromPlist = async (key) => path ? (await run("plutil", ["-extract", key, "raw", join(path, "Info.plist")], { timeoutMs: 10_000 }).catch(() => undefined))?.stdout.trim() || undefined : undefined;
|
|
22
|
+
const name = (await fromPlist("CFBundleDisplayName")) ?? (await fromPlist("CFBundleName")) ?? plistValue(stdout, "CFBundleDisplayName") ?? plistValue(stdout, "CFBundleName");
|
|
23
|
+
return { executable: (await fromPlist("CFBundleExecutable")) ?? plistValue(stdout, "CFBundleExecutable"), name };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** A bundle id as Apple allows it, so it can name files safely. */
|
|
30
|
+
export const BUNDLE_ID = /^[A-Za-z0-9.-]+$/;
|
|
31
|
+
/** Reads the bundle id from a built .app, so `install` can name what it installed. */
|
|
32
|
+
export async function bundleIdOf(appPath) {
|
|
33
|
+
const plist = join(appPath, "Info.plist");
|
|
34
|
+
if (!existsSync(plist))
|
|
35
|
+
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.");
|
|
36
|
+
const { stdout } = await run("plutil", ["-extract", "CFBundleIdentifier", "raw", plist], { timeoutMs: 10_000 });
|
|
37
|
+
return stdout.trim();
|
|
38
|
+
}
|
|
39
|
+
export async function install(udid, appPath) {
|
|
40
|
+
try {
|
|
41
|
+
await simctl(["install", udid, appPath], 120_000);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new ToolError(`Could not install ${appPath}: ${error.message}`, "Check it is built for the Simulator (iphonesimulator), not a device.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The file a launched app's print() output goes to, kept while the app runs. */
|
|
48
|
+
export async function consoleFile(udid, bundleId) {
|
|
49
|
+
return join(await deviceDir(udid), `${bundleId}.console.log`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Launches fresh (any running copy is stopped) and keeps the app's print() output for app_logs.
|
|
53
|
+
* simctl's --stdout wrote nothing on iOS 26.5, so a detached `simctl launch --console-pty` holds the app's
|
|
54
|
+
* output instead, writing to a file until the app ends; NSUnbufferedIO is what Xcode sets so lines arrive at once.
|
|
55
|
+
*/
|
|
56
|
+
export async function launch(udid, bundleId) {
|
|
57
|
+
const file = await consoleFile(udid, bundleId);
|
|
58
|
+
const fd = openSync(file, "w");
|
|
59
|
+
const child = spawn("xcrun", ["simctl", "launch", "--console-pty", "--terminate-running-process", udid, bundleId], {
|
|
60
|
+
detached: true,
|
|
61
|
+
stdio: ["ignore", fd, fd],
|
|
62
|
+
env: { ...process.env, SIMCTL_CHILD_NSUnbufferedIO: "YES" },
|
|
63
|
+
});
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
// A launch that fails ends the launcher within moments; a running app keeps it alive.
|
|
66
|
+
const code = await new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(() => resolve(undefined), 2_000);
|
|
68
|
+
child.once("exit", (c) => (clearTimeout(timer), resolve(c)));
|
|
69
|
+
child.once("error", () => (clearTimeout(timer), resolve(-1)));
|
|
70
|
+
});
|
|
71
|
+
child.unref();
|
|
72
|
+
if (code !== undefined && code !== 0) {
|
|
73
|
+
const output = readFileSync(file, "utf8").trim();
|
|
74
|
+
if (/not installed|FBSOpenApplicationServiceErrorDomain|Unable to find|could not be found/i.test(output)) {
|
|
75
|
+
throw new ToolError(`${bundleId} is not installed on this Simulator.`, 'Install it first: app_control {action: "install", app: "path/to/App.app"}.');
|
|
76
|
+
}
|
|
77
|
+
throw new ToolError(`Could not launch ${bundleId}: ${output.split("\n").slice(-3).join(" ")}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The app's own system log lines, streamed to a file while simulator-use watches it. */
|
|
81
|
+
export async function streamFile(udid, bundleId) {
|
|
82
|
+
return join(await deviceDir(udid), `${bundleId}.syslog.log`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The system log predicate for what an app itself logged: its own sender or subsystem. Apple's frameworks
|
|
86
|
+
* also log faults from inside the app (on iOS 27, a keyboard queue timeout on every first focus); a grep
|
|
87
|
+
* reaches those, and a crash is in the crash report and the print output.
|
|
88
|
+
*/
|
|
89
|
+
export function ownLines(executable, bundleId) {
|
|
90
|
+
const name = executable.replace(/"/g, "");
|
|
91
|
+
return `process == "${name}" AND (sender == "${name}" OR subsystem BEGINSWITH "${bundleId.replace(/"/g, "")}")`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Streams the app's own log lines to a file from before it launches. \`log show\` is not enough: macOS keeps
|
|
95
|
+
* info lines only in memory, and the Simulator's UIKit noise pushed the sample app's "signed in" line out
|
|
96
|
+
* within 30 s (October 19). One stream per app and device; a new launch replaces the old stream.
|
|
97
|
+
*/
|
|
98
|
+
export async function startLogStream(udid, bundleId, executable) {
|
|
99
|
+
await stopLogStream(udid, bundleId);
|
|
100
|
+
const file = await streamFile(udid, bundleId);
|
|
101
|
+
const fd = openSync(file, "w");
|
|
102
|
+
const child = spawn("xcrun", ["simctl", "spawn", udid, "log", "stream", "--style", "compact", "--level", "info", "--predicate", ownLines(executable, bundleId)], {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: ["ignore", fd, "ignore"],
|
|
105
|
+
});
|
|
106
|
+
closeSync(fd);
|
|
107
|
+
child.unref();
|
|
108
|
+
if (child.pid)
|
|
109
|
+
writeFileSync(`${file}.pid`, String(child.pid));
|
|
110
|
+
// The stream needs a moment to attach before the app's first lines.
|
|
111
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
112
|
+
}
|
|
113
|
+
export async function stopLogStream(udid, bundleId) {
|
|
114
|
+
const pidFile = `${await streamFile(udid, bundleId)}.pid`;
|
|
115
|
+
const pid = existsSync(pidFile) ? Number(readFileSync(pidFile, "utf8")) : 0;
|
|
116
|
+
if (await isOurs(pid, /simctl spawn .* log stream/)) {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(pid);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Gone between the check and the signal.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
rmSync(pidFile, { force: true });
|
|
125
|
+
}
|
|
126
|
+
export async function simctlAction(args, what, timeoutMs = 30_000) {
|
|
127
|
+
try {
|
|
128
|
+
return (await simctl(args, timeoutMs)).stdout.trim();
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
throw new ToolError(`Could not ${what}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function pushPayload(udid, bundleId, payload) {
|
|
135
|
+
await new Promise((resolve, reject) => {
|
|
136
|
+
const child = spawn("xcrun", ["simctl", "push", udid, bundleId, "-"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
137
|
+
let stderr = "";
|
|
138
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
139
|
+
child.on("error", reject);
|
|
140
|
+
child.on("close", (code) => (code === 0 ? resolve() : reject(new ToolError(`Could not send the push: ${stderr.trim()}`))));
|
|
141
|
+
child.stdin.end(payload);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** Puts text on the Simulator's clipboard, for pasting what the keyboard can't type (é, 日本, emoji). */
|
|
145
|
+
export async function setPasteboard(udid, text) {
|
|
146
|
+
try {
|
|
147
|
+
await new Promise((resolve, reject) => {
|
|
148
|
+
const child = spawn("xcrun", ["simctl", "pbcopy", udid], { stdio: ["pipe", "ignore", "pipe"] });
|
|
149
|
+
let stderr = "";
|
|
150
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
151
|
+
child.on("error", reject);
|
|
152
|
+
child.on("close", (code, signal) => (code === 0 ? resolve() : reject(new ToolError(`Could not set the Simulator's clipboard: ${stderr.trim() || signal || `exit ${code}`}`))));
|
|
153
|
+
child.stdin.end(text);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// A macOS user with no screen session (a shared Simulator host's users) has no pasteboard server, and simctl
|
|
158
|
+
// pbcopy crashes: the helper sets the phone's own pasteboard from inside the phone instead.
|
|
159
|
+
const helper = pasteHelper();
|
|
160
|
+
if (!helper)
|
|
161
|
+
throw error;
|
|
162
|
+
try {
|
|
163
|
+
await simctl(["spawn", udid, helper, text], 15_000);
|
|
164
|
+
}
|
|
165
|
+
catch (fallback) {
|
|
166
|
+
throw new ToolError(`Could not set the Simulator's clipboard: ${error.message}; from inside the phone: ${fallback.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** native/pbset, built for the Simulator, beside the engine in vendor/axe; undefined when it is not there. */
|
|
171
|
+
export function pasteHelper() {
|
|
172
|
+
const bundled = fileURLToPath(new URL("../../vendor/axe/simulator-use-pbset", import.meta.url));
|
|
173
|
+
return existsSync(bundled) ? bundled : undefined;
|
|
174
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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 { engineCall, noteSessionClosed, engineGesture, engineSteps } from "./native.js";
|
|
9
|
+
import { setting } from "../settings.js";
|
|
10
|
+
/**
|
|
11
|
+
* AXe (MIT, github.com/cameroncooke/AXe) reads the Simulator's accessibility tree and sends touches and keys,
|
|
12
|
+
* with no test runner to build. The package carries a pinned copy in vendor/axe; SIMULATOR_USE_AXE overrides it.
|
|
13
|
+
*/
|
|
14
|
+
export function axePath() {
|
|
15
|
+
const override = setting("AXE");
|
|
16
|
+
if (override)
|
|
17
|
+
return override;
|
|
18
|
+
// src/engine and dist/engine are both two levels below the package root.
|
|
19
|
+
const bundled = fileURLToPath(new URL("../../vendor/axe/axe", import.meta.url));
|
|
20
|
+
return existsSync(bundled) ? bundled : "axe";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* After a Simulator's first boot, its accessibility service takes a while to accept a reader: on a fresh
|
|
24
|
+
* Xcode 27 Mac, describe-ui failed with this for about 75 s, then worked.
|
|
25
|
+
*/
|
|
26
|
+
export class AccessibilityStarting extends ToolError {
|
|
27
|
+
}
|
|
28
|
+
// Also right after a boot: "No translation object returned for simulator" (iPhone 17e, 5 s after booting).
|
|
29
|
+
const STARTING = /remote automation session|No translation object returned/i;
|
|
30
|
+
export async function describeUi(udid) {
|
|
31
|
+
// A web page's first read can take half a minute (Safari, iOS 27): the engine gets a minute.
|
|
32
|
+
const answer = await engineCall(udid, { op: "describe" }, 60_000);
|
|
33
|
+
if (answer?.ok) {
|
|
34
|
+
const tree = answer.tree;
|
|
35
|
+
return Array.isArray(tree) ? tree : [tree];
|
|
36
|
+
}
|
|
37
|
+
if (answer?.busy)
|
|
38
|
+
throw new ToolError("Reading the screen took more than a minute.", "Call ui_look again: the phone is still working on the last read.");
|
|
39
|
+
if (answer && STARTING.test(answer.error ?? "")) {
|
|
40
|
+
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.");
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await run(axePath(), ["describe-ui", "--udid", udid], { timeoutMs: 30_000 });
|
|
44
|
+
noteSessionClosed(udid);
|
|
45
|
+
const parsed = JSON.parse(stdout);
|
|
46
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
noteSessionClosed(udid);
|
|
50
|
+
if (error.code === "ENOENT")
|
|
51
|
+
throw new ToolError("AXe is missing from this install.", "Reinstall simulator-use, or set SIMULATOR_USE_AXE to an axe binary.");
|
|
52
|
+
if (STARTING.test(error.message)) {
|
|
53
|
+
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.");
|
|
54
|
+
}
|
|
55
|
+
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.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 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). */
|
|
59
|
+
export async function screenshot(udid, points) {
|
|
60
|
+
const sized = points.width > 0 && points.height > 0 ? { width: Math.round(points.width), height: Math.round(points.height) } : {};
|
|
61
|
+
const answer = await engineCall(udid, { op: "screenshot", ...sized, quality: 0.7, inline: true });
|
|
62
|
+
if (answer?.ok && typeof answer.jpeg_base64 === "string")
|
|
63
|
+
return Buffer.from(answer.jpeg_base64, "base64");
|
|
64
|
+
if (answer?.busy)
|
|
65
|
+
throw new ToolError("The screenshot took more than 30 s.", "Take it again: the phone is still working on an earlier request.");
|
|
66
|
+
const dir = await mkdtemp(join(tmpdir(), "simulator-use-"));
|
|
67
|
+
try {
|
|
68
|
+
const png = join(dir, "screen.png");
|
|
69
|
+
const jpg = join(dir, "screen.jpg");
|
|
70
|
+
await run(axePath(), ["screenshot", "--output", png, "--udid", udid], { timeoutMs: 30_000 });
|
|
71
|
+
const size = points.width > 0 && points.height > 0 ? ["-z", String(Math.round(points.height)), String(Math.round(points.width))] : [];
|
|
72
|
+
await run("sips", [...size, "-s", "format", "jpeg", "-s", "formatOptions", "70", png, "--out", jpg], { timeoutMs: 30_000 });
|
|
73
|
+
return await readFile(jpg);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new ToolError(`Could not take a screenshot: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await rm(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Any AXe command may open and close an automation session of its own (a slider is found by its id), so each one is
|
|
84
|
+
* followed by the time the next engine waits from (noteSessionClosed).
|
|
85
|
+
*/
|
|
86
|
+
const axe = (udid, args, timeoutMs = 30_000) => run(axePath(), [...args, "--udid", udid], { timeoutMs }).finally(() => noteSessionClosed(udid));
|
|
87
|
+
/** One AXe process for several input steps (tap, type, key, key-combo, swipe, sleep): about 0.2 s cheaper per step. */
|
|
88
|
+
export async function batch(udid, steps) {
|
|
89
|
+
const native = engineSteps(steps);
|
|
90
|
+
if (native) {
|
|
91
|
+
const answer = await engineCall(udid, { op: "steps", steps: native }, 60_000);
|
|
92
|
+
if (answer?.ok)
|
|
93
|
+
return;
|
|
94
|
+
if (answer?.busy)
|
|
95
|
+
throw new ToolError("The Simulator did not take the input within a minute.");
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await axe(udid, ["batch", "--ax-cache", "none", "--type-submission", "composite", ...steps.flatMap((s) => ["--step", s])], 60_000);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new ToolError(`The Simulator did not take the input: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** A drag or swipe on its own: AXe's batch takes swipe but not drag. */
|
|
105
|
+
export async function gesture(udid, args) {
|
|
106
|
+
const native = engineGesture(args);
|
|
107
|
+
if (native) {
|
|
108
|
+
const answer = await engineCall(udid, native, 30_000);
|
|
109
|
+
if (answer?.ok)
|
|
110
|
+
return;
|
|
111
|
+
if (answer?.busy)
|
|
112
|
+
throw new ToolError("The Simulator did not take the gesture within 30 s.");
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await axe(udid, args, 30_000);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
throw new ToolError(`The Simulator did not take the gesture: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** The single element at a point, for reading back a field right after typing (about 0.15 s). */
|
|
122
|
+
export async function elementAt(udid, x, y) {
|
|
123
|
+
const answer = await engineCall(udid, { op: "point", x: Math.round(x), y: Math.round(y) }, 3_000);
|
|
124
|
+
if (answer?.ok) {
|
|
125
|
+
const element = answer.element;
|
|
126
|
+
return Array.isArray(element) ? element[0] : element;
|
|
127
|
+
}
|
|
128
|
+
// Slow (a web field just focused answered in 4.2 s): the caller reads the whole screen instead.
|
|
129
|
+
if (answer?.busy)
|
|
130
|
+
return undefined;
|
|
131
|
+
try {
|
|
132
|
+
// 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.
|
|
133
|
+
const { stdout } = await axe(udid, ["describe-ui", "--point", `${Math.round(x)},${Math.round(y)}`], 3_000);
|
|
134
|
+
noteSessionClosed(udid);
|
|
135
|
+
const parsed = JSON.parse(stdout);
|
|
136
|
+
return Array.isArray(parsed) ? parsed[0] : parsed;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
noteSessionClosed(udid);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** A step string for AXe batch: text goes in single quotes, with any single quote escaped the shell way. */
|
|
144
|
+
export function typeStep(text) {
|
|
145
|
+
return `type '${text.replace(/'/g, `'\\''`)}'`;
|
|
146
|
+
}
|
|
147
|
+
export async function pressButton(udid, button) {
|
|
148
|
+
const answer = await engineCall(udid, { op: button, wait: false });
|
|
149
|
+
if (answer?.ok)
|
|
150
|
+
return;
|
|
151
|
+
if (answer?.busy)
|
|
152
|
+
throw new ToolError(`The ${button} button press was not answered within 30 s.`);
|
|
153
|
+
await axe(udid, ["button", button]);
|
|
154
|
+
}
|
|
155
|
+
/** Sets a slider by accessibility id or label to a percent; AXe drags it there. */
|
|
156
|
+
export async function setSlider(udid, target, percent) {
|
|
157
|
+
const by = target.id ? ["--id", target.id] : ["--label", target.label ?? ""];
|
|
158
|
+
try {
|
|
159
|
+
await axe(udid, ["slider", ...by, "--value", String(percent)]);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw new ToolError(`Could not set the slider: ${error.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -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
|
+
}
|