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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { stateDir } from "./state.js";
|
|
5
|
+
const alive = (pid) => {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(pid, 0);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return error.code === "EPERM";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/** A lock's directory: names that are not plain words (a project's path) are hashed. */
|
|
15
|
+
const lockDir = (name) => join(stateDir(), `${/^[\w.-]{1,80}$/.test(name) ? name : createHash("sha1").update(name).digest("hex").slice(0, 16)}.lock.d`);
|
|
16
|
+
/**
|
|
17
|
+
* Takes a lock shared by every simulator-use process on this Mac (each agent, and each CLI call, is its own process).
|
|
18
|
+
* The lock is a directory, made atomically, holding the owner's pid; a lock whose owner died, or that was left
|
|
19
|
+
* without a pid for 60 s, is taken over. Returns its release, or the pid that holds it.
|
|
20
|
+
*/
|
|
21
|
+
export async function tryLock(name) {
|
|
22
|
+
await mkdir(stateDir(), { recursive: true });
|
|
23
|
+
const lock = lockDir(name);
|
|
24
|
+
const owner = join(lock, "pid");
|
|
25
|
+
for (;;) {
|
|
26
|
+
try {
|
|
27
|
+
await mkdir(lock);
|
|
28
|
+
await writeFile(owner, String(process.pid));
|
|
29
|
+
return {
|
|
30
|
+
// Only its owner removes it: one taken over from a dead owner belongs to someone else now.
|
|
31
|
+
release: async () => {
|
|
32
|
+
if (Number(await readFile(owner, "utf8").catch(() => "0")) === process.pid)
|
|
33
|
+
await rm(lock, { recursive: true, force: true });
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== "EEXIST")
|
|
39
|
+
throw error;
|
|
40
|
+
const holder = Number(await readFile(owner, "utf8").catch(() => "0"));
|
|
41
|
+
const age = Date.now() - ((await stat(lock).catch(() => undefined))?.mtimeMs ?? Date.now());
|
|
42
|
+
// An empty pid is a lock being made right now, unless it has been empty for a minute.
|
|
43
|
+
if ((holder && !alive(holder)) || (!holder && age > 60_000)) {
|
|
44
|
+
await rm(lock, { recursive: true, force: true });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return { holder };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Runs work holding the lock, waiting for it up to waitMs; past that, busy() says why it can't. */
|
|
52
|
+
export async function withLock(name, waitMs, busy, work) {
|
|
53
|
+
const deadline = Date.now() + waitMs;
|
|
54
|
+
for (;;) {
|
|
55
|
+
const taken = await tryLock(name);
|
|
56
|
+
if ("release" in taken) {
|
|
57
|
+
try {
|
|
58
|
+
return await work();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await taken.release();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (Date.now() > deadline)
|
|
65
|
+
throw busy(taken.holder);
|
|
66
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ownLines } from "./apps.js";
|
|
5
|
+
import { run } from "./exec.js";
|
|
6
|
+
const LEVELS = { Df: "", I: "info", Db: "debug", E: "error", F: "fault" };
|
|
7
|
+
/** `2026-10-19 01:56:22.114 E Demo[94957:88904] (Sender) [subsystem:category] message`; sender and subsystem are optional. */
|
|
8
|
+
const COMPACT = /^\d{4}-\d\d-\d\d (\d\d:\d\d:\d\d\.\d+) (\w+)\s+\S+?\[\d+:\w+\] (?:\(([^)]+)\) )?(?:\[([^\]]+)\] )?(.*)$/;
|
|
9
|
+
/** Parses `log show --style compact`. Activity lines (type A) are bookkeeping, never kept. */
|
|
10
|
+
export function parseSystemLog(output, appSubsystem) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
for (const raw of output.split("\n")) {
|
|
13
|
+
const m = COMPACT.exec(raw);
|
|
14
|
+
if (!m || m[2] === "A")
|
|
15
|
+
continue;
|
|
16
|
+
const [, time, type, sender, source = "", text = ""] = m;
|
|
17
|
+
const [subsystem = "", category = ""] = source.split(":");
|
|
18
|
+
const tag = subsystem && appSubsystem && subsystem.startsWith(appSubsystem) ? category : source || (sender ? `(${sender})` : "");
|
|
19
|
+
lines.push({ time: time, level: LEVELS[type] ?? type, text: `${tag ? `${tag}: ` : ""}${text}` });
|
|
20
|
+
}
|
|
21
|
+
return lines;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The app's lines from the Simulator's system log. Apple's frameworks log a great deal from inside the app's
|
|
25
|
+
* process (UIKit's event dispatch, several lines a tap; on the Simulator, a haptics error every key press), so
|
|
26
|
+
* by default only what the app itself logged is read, and faults from anywhere; `everything` reads it all.
|
|
27
|
+
*/
|
|
28
|
+
export async function systemLog(udid, executable, seconds, bundleId, everything = false) {
|
|
29
|
+
const predicate = everything ? `process == "${executable.replace(/"/g, "")}"` : ownLines(executable, bundleId);
|
|
30
|
+
const { stdout } = await run("xcrun", ["simctl", "spawn", udid, "log", "show", "--style", "compact", "--info", "--last", `${Math.max(1, Math.ceil(seconds))}s`, "--predicate", predicate], {
|
|
31
|
+
timeoutMs: 12_000,
|
|
32
|
+
});
|
|
33
|
+
return parseSystemLog(stdout, bundleId);
|
|
34
|
+
}
|
|
35
|
+
/** Lines from the app's own log stream (startLogStream) that are newer than `since` (ms since 1970). */
|
|
36
|
+
export async function streamedLines(file, since, bundleId) {
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = await readFile(file, "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const lines = [];
|
|
45
|
+
for (const raw of text.split("\n")) {
|
|
46
|
+
// The stamp is the Mac's local time, as `2026-10-19 02:04:48.818`.
|
|
47
|
+
const stamp = /^(\d{4}-\d\d-\d\d) (\d\d:\d\d:\d\d\.\d+)/.exec(raw);
|
|
48
|
+
if (!stamp || new Date(`${stamp[1]}T${stamp[2]}`).getTime() < since)
|
|
49
|
+
continue;
|
|
50
|
+
lines.push(...parseSystemLog(raw, bundleId));
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
/** The app's print() output, without the Simulator's own start-up noise. */
|
|
55
|
+
export function cleanConsole(text) {
|
|
56
|
+
return text
|
|
57
|
+
.replace(/\r/g, "")
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((line) => line.trim() && !/^objc\[\d+\]: Class .* is implemented in both/.test(line));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Crash reports of a Simulator app land on the Mac itself, in ~/Library/Logs/DiagnosticReports, named after
|
|
63
|
+
* the app's executable. An .ips file is one JSON header line, then the report as JSON.
|
|
64
|
+
*/
|
|
65
|
+
export async function crashReports(executable, since, limit = 3) {
|
|
66
|
+
const dir = join(homedir(), "Library", "Logs", "DiagnosticReports");
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
names = (await readdir(dir)).filter((n) => n.startsWith(`${executable}-`) && n.endsWith(".ips"));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const files = await Promise.all(names.map(async (n) => ({ file: join(dir, n), at: (await stat(join(dir, n))).mtimeMs })));
|
|
75
|
+
const recent = files.filter((f) => f.at >= since).sort((a, b) => b.at - a.at).slice(0, limit);
|
|
76
|
+
return Promise.all(recent.map(async (f) => {
|
|
77
|
+
const text = await readFile(f.file, "utf8");
|
|
78
|
+
// When it crashed, not when macOS finished writing the report (up to a minute later).
|
|
79
|
+
const time = /"captureTime"\s*:\s*"([^"]+)"/.exec(text)?.[1] ?? new Date(f.at).toISOString();
|
|
80
|
+
return { file: f.file, time, summary: summarizeIps(text, executable) };
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What went wrong and where: the exception, the reason, and the crashing thread from the top down to just past
|
|
85
|
+
* the app's own last frame (the rest is the framework that called it).
|
|
86
|
+
*/
|
|
87
|
+
export function summarizeIps(text, executable) {
|
|
88
|
+
const body = text.slice(text.indexOf("\n") + 1);
|
|
89
|
+
let ips;
|
|
90
|
+
try {
|
|
91
|
+
ips = JSON.parse(body);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return ["(unreadable crash report)"];
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
if (ips.exception)
|
|
98
|
+
out.push(`exception: ${[ips.exception.type, ips.exception.signal].filter(Boolean).join(" ")}`);
|
|
99
|
+
const reasons = [...(ips.termination?.reasons ?? []), ...Object.values(ips.asi ?? {}).flat(), ips.termination?.indicator ?? ""].filter(Boolean);
|
|
100
|
+
for (const reason of reasons.slice(0, 3))
|
|
101
|
+
out.push(`reason: ${reason.replace(/\s+/g, " ").trim()}`);
|
|
102
|
+
const thread = ips.threads?.find((t) => t.triggered) ?? ips.threads?.[ips.faultingThread ?? 0];
|
|
103
|
+
const all = thread?.frames ?? [];
|
|
104
|
+
const imageOf = (f) => ips.usedImages?.[f.imageIndex ?? -1]?.name ?? "?";
|
|
105
|
+
const own = (f) => Boolean(executable) && (imageOf(f) === executable || imageOf(f).startsWith(`${executable}.`));
|
|
106
|
+
const lastOwn = all.reduce((last, f, i) => (own(f) ? i : last), -1);
|
|
107
|
+
const frames = all.slice(0, lastOwn >= 0 ? Math.min(lastOwn + 3, 12) : 8).map((f) => {
|
|
108
|
+
const image = imageOf(f);
|
|
109
|
+
const where = f.sourceFile ? ` (${f.sourceFile}:${f.sourceLine ?? "?"})` : "";
|
|
110
|
+
return ` ${image} ${f.symbol ?? `+${f.imageOffset ?? 0}`}${where}`;
|
|
111
|
+
});
|
|
112
|
+
if (frames.length)
|
|
113
|
+
out.push("crashed thread:", ...frames);
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { withCloudHint } from "../cloud-hint.js";
|
|
2
|
+
import { ToolError } from "../errors.js";
|
|
3
|
+
import { run } from "./exec.js";
|
|
4
|
+
import { withLock } from "./lock.js";
|
|
5
|
+
const NO_SIMULATOR = "This computer can't run the iOS Simulator: it needs macOS with Xcode and a Simulator runtime.";
|
|
6
|
+
/** "com.apple.CoreSimulator.SimRuntime.iOS-26-5" becomes "iOS 26.5". */
|
|
7
|
+
export function runtimeName(identifier) {
|
|
8
|
+
const match = /SimRuntime\.([A-Za-z]+)-(\d+)-(\d+)(?:-(\d+))?$/.exec(identifier);
|
|
9
|
+
if (!match)
|
|
10
|
+
return identifier;
|
|
11
|
+
const [, os, major, minor, patch] = match;
|
|
12
|
+
return `${os === "xrOS" ? "visionOS" : os} ${major}.${minor}${patch ? `.${patch}` : ""}`;
|
|
13
|
+
}
|
|
14
|
+
/** Parses `simctl list devices --json`: available devices only, booted first, then by runtime and name. */
|
|
15
|
+
export function parseDevices(json) {
|
|
16
|
+
const parsed = JSON.parse(json);
|
|
17
|
+
const devices = [];
|
|
18
|
+
for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
|
|
19
|
+
for (const d of list) {
|
|
20
|
+
if (d.isAvailable === false)
|
|
21
|
+
continue;
|
|
22
|
+
devices.push({ udid: d.udid, name: d.name, state: d.state, runtime: runtimeName(runtime), booted: d.state === "Booted" });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// iOS before watchOS, tvOS and visionOS (by name, "watchOS 26" would sort first); the newest iOS first;
|
|
26
|
+
// iPhones before iPads: most agents want a phone.
|
|
27
|
+
const platform = (d) => (d.runtime.startsWith("iOS ") ? 0 : 1);
|
|
28
|
+
const version = (d) => d.runtime.replace(/^\S+\s/, "");
|
|
29
|
+
const family = (d) => ["iPhone", "iPad"].findIndex((p) => d.name.startsWith(p)) >>> 0;
|
|
30
|
+
return devices.sort((a, b) => Number(b.booted) - Number(a.booted) ||
|
|
31
|
+
platform(a) - platform(b) ||
|
|
32
|
+
version(b).localeCompare(version(a), undefined, { numeric: true }) ||
|
|
33
|
+
family(a) - family(b) ||
|
|
34
|
+
a.name.localeCompare(b.name, undefined, { numeric: true }));
|
|
35
|
+
}
|
|
36
|
+
/** The MCP server lives for a whole session: one list serves the calls of the next few seconds (0.35 s saved each). */
|
|
37
|
+
const CACHE_MS = 5_000;
|
|
38
|
+
let cached;
|
|
39
|
+
export async function listDevices(fresh = false) {
|
|
40
|
+
if (process.platform !== "darwin")
|
|
41
|
+
throw new ToolError(NO_SIMULATOR, withCloudHint("Run simulator-use on a Mac with Xcode."));
|
|
42
|
+
if (!fresh && cached && Date.now() - cached.at < CACHE_MS)
|
|
43
|
+
return cached.devices;
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await run("xcrun", ["simctl", "list", "devices", "--json"], { timeoutMs: 30_000 });
|
|
46
|
+
cached = { at: Date.now(), devices: parseDevices(stdout) };
|
|
47
|
+
return cached.devices;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw listError(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Why simctl gave no device list. A timeout means the Simulator service is still starting (it can take a minute
|
|
55
|
+
* the first time after a reboot or an Xcode update), not that this computer can't run it.
|
|
56
|
+
*/
|
|
57
|
+
export function listError(error) {
|
|
58
|
+
if (/timed out after/.test(error.message)) {
|
|
59
|
+
return new ToolError("The Simulator service did not answer within 30 s: it can take a minute to start the first time after a reboot or an Xcode update.", "Call again in 30 s.");
|
|
60
|
+
}
|
|
61
|
+
return new ToolError(`${NO_SIMULATOR} (${error.message})`, withCloudHint("Install Xcode from the App Store, open it once, then add an iOS Simulator runtime in Settings > Components."));
|
|
62
|
+
}
|
|
63
|
+
export async function xcodeVersion() {
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await run("xcodebuild", ["-version"], { timeoutMs: 15_000 });
|
|
66
|
+
return stdout.split("\n")[0]?.trim();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** A device by name or UDID; a name on several runtimes means the booted one, else the newest. */
|
|
73
|
+
export function findDevice(devices, wanted) {
|
|
74
|
+
const want = wanted.trim().toLowerCase();
|
|
75
|
+
const matches = devices.filter((d) => d.udid.toLowerCase() === want || d.name.toLowerCase() === want);
|
|
76
|
+
return matches.find((d) => d.booted) ?? matches[0];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The device a call works on: the one named (by name or UDID), else the one this agent holds (device_use),
|
|
80
|
+
* else the only booted one.
|
|
81
|
+
*/
|
|
82
|
+
export function pickDevice(devices, wanted, held) {
|
|
83
|
+
const booted = devices.filter((d) => d.booted);
|
|
84
|
+
if (wanted) {
|
|
85
|
+
const device = findDevice(devices, wanted);
|
|
86
|
+
if (!device)
|
|
87
|
+
throw new ToolError(`No Simulator named "${wanted}".`, "Call devices to see the names and UDIDs on this Mac.");
|
|
88
|
+
if (!device.booted)
|
|
89
|
+
throw new ToolError(`${device.name} (${device.runtime}) is not booted.`, `Call device_use {device: "${device.udid}"} to boot it.`);
|
|
90
|
+
return device;
|
|
91
|
+
}
|
|
92
|
+
const mine = held && booted.find((d) => d.udid === held);
|
|
93
|
+
if (mine)
|
|
94
|
+
return mine;
|
|
95
|
+
// The device this agent chose was shut down (by another agent's boot, say): never slide onto someone else's.
|
|
96
|
+
const gone = held && devices.find((d) => d.udid === held);
|
|
97
|
+
if (gone)
|
|
98
|
+
throw new ToolError(`${gone.name} (${gone.runtime}), the Simulator this agent was using, is no longer booted.`, `Call device_use {device: "${gone.udid}"} to boot it again, or device_use another.`);
|
|
99
|
+
if (booted.length === 1)
|
|
100
|
+
return booted[0];
|
|
101
|
+
if (!booted.length) {
|
|
102
|
+
const suggestion = devices.find((d) => d.name.startsWith("iPhone")) ?? devices[0];
|
|
103
|
+
throw new ToolError("No Simulator is booted.", suggestion ? `Call device_use {device: "${suggestion.name}"} to boot ${suggestion.name} (${suggestion.runtime}).` : "Call devices to see what this Mac has.");
|
|
104
|
+
}
|
|
105
|
+
throw new ToolError(`${booted.length} Simulators are booted: ${booted.map((d) => `${d.name} (${d.udid})`).join(", ")}.`, "Call device_use with the one to use, or pass device.");
|
|
106
|
+
}
|
|
107
|
+
/** A call may take 45 s: waiting for another boot and this boot share 40 of them. */
|
|
108
|
+
const BOOT_BUDGET_MS = 40_000;
|
|
109
|
+
const LOCK_WAIT_MS = 20_000;
|
|
110
|
+
/**
|
|
111
|
+
* One boot at a time on this Mac, across every simulator-use process (each agent runs its own), held in a lock
|
|
112
|
+
* (lock.ts) that a dead owner's successor takes over.
|
|
113
|
+
*/
|
|
114
|
+
const withBootLock = (work) => withLock("boot", LOCK_WAIT_MS, () => new ToolError("Another Simulator is still booting, and boots never overlap.", "Call device_use again in 30 s."), work);
|
|
115
|
+
/**
|
|
116
|
+
* Boots a device and waits until it is ready, within the time a call may take. Two Simulators booting at once
|
|
117
|
+
* on an 8 GB Mac pushed it into swap for minutes (measured October 18), so boots never overlap. With a limit on
|
|
118
|
+
* booted devices (SIMULATOR_USE_MAX_BOOTED), others are shut down first, inside the lock so two calls can't both
|
|
119
|
+
* see room.
|
|
120
|
+
*/
|
|
121
|
+
export async function bootDevice(device, maxBooted = Infinity) {
|
|
122
|
+
const asked = Date.now();
|
|
123
|
+
return withBootLock(async () => {
|
|
124
|
+
const started = Date.now();
|
|
125
|
+
// A boot started outside simulator-use (Xcode, simctl) is waited out too.
|
|
126
|
+
while (Date.now() - asked < LOCK_WAIT_MS && (await listDevices(true)).some((d) => d.state === "Booting" && d.udid !== device.udid)) {
|
|
127
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
128
|
+
}
|
|
129
|
+
const others = (await listDevices(true)).filter((d) => d.booted && d.udid !== device.udid);
|
|
130
|
+
const shutDown = [];
|
|
131
|
+
for (const d of others.slice(0, Math.max(0, others.length - (maxBooted - 1)))) {
|
|
132
|
+
await shutdownDevice(d);
|
|
133
|
+
shutDown.push(d.name);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
await run("xcrun", ["simctl", "boot", device.udid], { timeoutMs: 20_000 });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (!/current state: Booted/i.test(error.message))
|
|
140
|
+
throw new ToolError(`Could not boot ${device.name}: ${error.message}`);
|
|
141
|
+
}
|
|
142
|
+
let ready = true;
|
|
143
|
+
try {
|
|
144
|
+
await run("xcrun", ["simctl", "bootstatus", device.udid, "-b"], { timeoutMs: Math.max(5_000, BOOT_BUDGET_MS - (Date.now() - asked)) });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
ready = false;
|
|
148
|
+
}
|
|
149
|
+
cached = undefined;
|
|
150
|
+
return { ms: Date.now() - started, ready, waitedMs: started - asked, shutDown };
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export async function shutdownDevice(device) {
|
|
154
|
+
await run("xcrun", ["simctl", "shutdown", device.udid], { timeoutMs: 15_000 }).catch(() => undefined);
|
|
155
|
+
cached = undefined;
|
|
156
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { setting } from "../settings.js";
|
|
6
|
+
/**
|
|
7
|
+
* What a call can leave for the next one.
|
|
8
|
+
* - The device an MCP server's agent holds lives in its memory: each agent runs its own server, so two agents
|
|
9
|
+
* never share it. A CLI call is its own process, so the CLI keeps the device in a file (persistHeld), and
|
|
10
|
+
* `simulator-use use` carries over to the next command.
|
|
11
|
+
* - The app last launched on a device is a small file next to its log files, so the CLI and MCP see the same.
|
|
12
|
+
*/
|
|
13
|
+
let held;
|
|
14
|
+
let persist = false;
|
|
15
|
+
/** Where simulator-use keeps its files: SIMULATOR_USE_STATE_DIR, else simulator-use in the user's temporary folder. */
|
|
16
|
+
export const stateDir = () => setting("STATE_DIR") || join(tmpdir(), "simulator-use");
|
|
17
|
+
const heldFile = () => join(stateDir(), "held-device.json");
|
|
18
|
+
/** The CLI keeps the held device between its calls. */
|
|
19
|
+
export function persistHeld() {
|
|
20
|
+
persist = true;
|
|
21
|
+
}
|
|
22
|
+
export const holdDevice = (udid) => {
|
|
23
|
+
held = udid;
|
|
24
|
+
if (!persist)
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
28
|
+
writeFileSync(heldFile(), JSON.stringify({ udid }));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// The next call falls back to the booted device.
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const heldDevice = () => {
|
|
35
|
+
if (held || !persist)
|
|
36
|
+
return held;
|
|
37
|
+
try {
|
|
38
|
+
const { udid } = JSON.parse(readFileSync(heldFile(), "utf8"));
|
|
39
|
+
if (typeof udid === "string" && /^[0-9A-Fa-f-]{36}$/.test(udid))
|
|
40
|
+
held = udid;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// None held yet.
|
|
44
|
+
}
|
|
45
|
+
return held;
|
|
46
|
+
};
|
|
47
|
+
/** Where simulator-use keeps a device's app output: the print() lines of apps it launched. */
|
|
48
|
+
export async function deviceDir(udid) {
|
|
49
|
+
const dir = join(stateDir(), udid);
|
|
50
|
+
await mkdir(dir, { recursive: true });
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
export async function currentApp(udid) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(await readFile(join(await deviceDir(udid), "app.json"), "utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function setCurrentApp(udid, app) {
|
|
62
|
+
await writeFile(join(await deviceDir(udid), "app.json"), JSON.stringify(app));
|
|
63
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createWriteStream, existsSync, readdirSync, realpathSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { ToolError } from "../errors.js";
|
|
7
|
+
import { run } from "./exec.js";
|
|
8
|
+
import { stateDir } from "./state.js";
|
|
9
|
+
import { setting } from "../settings.js";
|
|
10
|
+
/**
|
|
11
|
+
* The Xcode project in a folder: a workspace first (CocoaPods and multi-project setups need it), then a
|
|
12
|
+
* project; an XcodeGen spec is generated first, and again before every build. Other kinds say what is coming.
|
|
13
|
+
*/
|
|
14
|
+
export async function findProject(where) {
|
|
15
|
+
const path = resolve(where);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
throw new ToolError(`${path} does not exist.`, "Pass path: the folder with the Xcode project.");
|
|
18
|
+
const ext = extname(path);
|
|
19
|
+
if (ext === ".xcworkspace")
|
|
20
|
+
return { kind: "workspace", path, name: basename(path, ext), dir: dirname(path) };
|
|
21
|
+
if (ext === ".xcodeproj") {
|
|
22
|
+
const dir = dirname(path);
|
|
23
|
+
const note = existsSync(join(dir, "project.yml")) ? await xcodegen(dir, true) : undefined;
|
|
24
|
+
return { kind: "project", path, name: basename(path, ext), dir, note };
|
|
25
|
+
}
|
|
26
|
+
const entries = readdirSync(path);
|
|
27
|
+
// A workspace is left as it is: regenerating its project would undo what CocoaPods added to it.
|
|
28
|
+
const workspace = entries.find((e) => e.endsWith(".xcworkspace"));
|
|
29
|
+
if (workspace)
|
|
30
|
+
return { kind: "workspace", path: join(path, workspace), name: basename(workspace, ".xcworkspace"), dir: path };
|
|
31
|
+
const project = entries.find((e) => e.endsWith(".xcodeproj"));
|
|
32
|
+
if (entries.includes("project.yml")) {
|
|
33
|
+
const note = await xcodegen(path, Boolean(project));
|
|
34
|
+
const made = readdirSync(path).find((e) => e.endsWith(".xcodeproj"));
|
|
35
|
+
if (made)
|
|
36
|
+
return { kind: "project", path: join(path, made), name: basename(made, ".xcodeproj"), dir: path, generated: !project, note };
|
|
37
|
+
}
|
|
38
|
+
if (project)
|
|
39
|
+
return { kind: "project", path: join(path, project), name: basename(project, ".xcodeproj"), dir: path };
|
|
40
|
+
if (entries.includes("app.json") || entries.includes("pubspec.yaml") || entries.some((e) => e.startsWith("capacitor.config"))) {
|
|
41
|
+
throw new ToolError("Expo, React Native, Flutter and Capacitor projects are not built by app_run yet; Xcode projects are.", "Build it with its own tool for the iOS Simulator, then app_control {action: \"install\", app: \"path/to/App.app\"}.");
|
|
42
|
+
}
|
|
43
|
+
if (entries.includes("ios") && existsSync(join(path, "ios")))
|
|
44
|
+
return findProject(join(path, "ios"));
|
|
45
|
+
throw new ToolError(`No Xcode project in ${path}.`, "Pass path: the folder with the .xcodeproj or .xcworkspace.");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* XcodeGen makes the project from project.yml, and a spec edit or a new, moved or deleted source file needs a new
|
|
49
|
+
* project, or the build misses it. So the project is regenerated before every build; --use-cache skips the work when
|
|
50
|
+
* neither the spec nor the files changed. Answers a note when an existing project had to be built as it was.
|
|
51
|
+
*/
|
|
52
|
+
async function xcodegen(dir, hasProject) {
|
|
53
|
+
try {
|
|
54
|
+
await run("xcodegen", ["generate", "--quiet", "--use-cache", "--spec", join(dir, "project.yml")], { timeoutMs: 60_000 });
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const why = (error.message.split("\n")[0] ?? "").slice(0, 200);
|
|
59
|
+
if (!hasProject)
|
|
60
|
+
throw new ToolError(`This folder has an XcodeGen spec (project.yml) but no project, and xcodegen failed: ${why}`, "Install XcodeGen (brew install xcodegen), or generate the project yourself.");
|
|
61
|
+
return `XcodeGen could not regenerate the project from project.yml (${why}), so the existing project was built; spec edits and new files are not in it.`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The scheme to build: the one asked for, the only one, or the one named like the project. */
|
|
65
|
+
export async function pickScheme(project, wanted) {
|
|
66
|
+
const { stdout } = await run("xcodebuild", ["-list", "-json", project.kind === "workspace" ? "-workspace" : "-project", project.path], { timeoutMs: 60_000 });
|
|
67
|
+
const list = JSON.parse(stdout);
|
|
68
|
+
const schemes = list.workspace?.schemes ?? list.project?.schemes ?? [];
|
|
69
|
+
if (wanted) {
|
|
70
|
+
if (!schemes.includes(wanted))
|
|
71
|
+
throw new ToolError(`No scheme "${wanted}" in ${basename(project.path)}. Schemes: ${schemes.join(", ") || "none"}.`);
|
|
72
|
+
return wanted;
|
|
73
|
+
}
|
|
74
|
+
const own = schemes.filter((s) => !/^Pods-|Tests$|UITests$/.test(s));
|
|
75
|
+
if (own.length === 1)
|
|
76
|
+
return own[0];
|
|
77
|
+
const named = own.find((s) => s === project.name);
|
|
78
|
+
if (named)
|
|
79
|
+
return named;
|
|
80
|
+
if (!schemes.length)
|
|
81
|
+
throw new ToolError(`${basename(project.path)} has no shared schemes.`, "Open it in Xcode once, or share a scheme (Product > Scheme > Manage Schemes).");
|
|
82
|
+
throw new ToolError(`${basename(project.path)} has several schemes: ${own.join(", ")}.`, "Pass scheme.");
|
|
83
|
+
}
|
|
84
|
+
const LOCATED = /^(\/[^:]+):(\d+):(?:(\d+):)? (error|warning|fatal error): (.+)$/;
|
|
85
|
+
const BARE = /^(?:xcodebuild: |clang: |ld: |\s*)?(error|fatal error): (.+)$/;
|
|
86
|
+
/** One line of xcodebuild output as an error or warning, or nothing. */
|
|
87
|
+
export function parseIssue(line) {
|
|
88
|
+
const located = LOCATED.exec(line);
|
|
89
|
+
if (located) {
|
|
90
|
+
const [, file, row, col, severity, message] = located;
|
|
91
|
+
return { file, line: Number(row), column: col ? Number(col) : undefined, severity: severity === "warning" ? "warning" : "error", message: message.trim() };
|
|
92
|
+
}
|
|
93
|
+
const bare = BARE.exec(line);
|
|
94
|
+
if (bare)
|
|
95
|
+
return { severity: "error", message: bare[2].trim() };
|
|
96
|
+
if (/^ld: /.test(line) || /^Undefined symbols? for architecture/.test(line))
|
|
97
|
+
return { severity: "error", message: line.trim() };
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
/** `Sources/App.swift:49:37`, relative to the project when the file is inside it, else absolute. */
|
|
101
|
+
export function place(file, line, column, base) {
|
|
102
|
+
const real = (p) => {
|
|
103
|
+
try {
|
|
104
|
+
return realpathSync(p);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
// /tmp and /private/tmp are the same folder; compare real paths so the file reads relative.
|
|
111
|
+
const rel = relative(real(base), real(file));
|
|
112
|
+
return `${rel && !rel.startsWith("..") ? rel : file}:${line ?? "?"}${column ? `:${column}` : ""}`;
|
|
113
|
+
}
|
|
114
|
+
/** `Sources/App.swift:49:37: error: ...`. */
|
|
115
|
+
export function formatIssue(issue, base) {
|
|
116
|
+
return `${issue.file ? `${place(issue.file, issue.line, issue.column, base)}: ` : ""}${issue.severity}: ${issue.message}`;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Where builds keep their products: SIMULATOR_USE_CACHE_DIR when set (a disk kept between machines, such as
|
|
120
|
+
* NoMac's ~/drive, makes the next machine's build incremental), else simulator-use's own folder.
|
|
121
|
+
*/
|
|
122
|
+
export const cacheDir = () => setting("CACHE_DIR") || stateDir();
|
|
123
|
+
/** Each worktree builds into its own DerivedData, so builds stay incremental and never share products. */
|
|
124
|
+
export function derivedDataFor(project) {
|
|
125
|
+
const key = createHash("sha1").update(project.path).digest("hex").slice(0, 12);
|
|
126
|
+
return join(cacheDir(), "DerivedData", `${project.name}-${key}`);
|
|
127
|
+
}
|
|
128
|
+
export async function build(project, scheme, udid, options) {
|
|
129
|
+
const derived = derivedDataFor(project);
|
|
130
|
+
await mkdir(derived, { recursive: true });
|
|
131
|
+
const log = join(derived, `${options.action ?? "build"}-${Date.now()}.log`);
|
|
132
|
+
const args = [
|
|
133
|
+
project.kind === "workspace" ? "-workspace" : "-project",
|
|
134
|
+
project.path,
|
|
135
|
+
"-scheme",
|
|
136
|
+
scheme,
|
|
137
|
+
"-configuration",
|
|
138
|
+
options.configuration ?? "Debug",
|
|
139
|
+
"-destination",
|
|
140
|
+
`id=${udid}`,
|
|
141
|
+
"-derivedDataPath",
|
|
142
|
+
derived,
|
|
143
|
+
...(options.extra ?? []),
|
|
144
|
+
...(options.clean ? ["clean"] : []),
|
|
145
|
+
options.action ?? "build",
|
|
146
|
+
];
|
|
147
|
+
const started = Date.now();
|
|
148
|
+
const out = createWriteStream(log);
|
|
149
|
+
// Its own process group, so a cancel stops the compilers it started too.
|
|
150
|
+
const child = spawn("xcodebuild", args, { cwd: project.dir, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
151
|
+
options.onCancel?.(() => {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(-child.pid, "SIGTERM");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Already finished.
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const errors = [];
|
|
161
|
+
let warnings = 0;
|
|
162
|
+
let pending = "";
|
|
163
|
+
const tail = [];
|
|
164
|
+
const take = (chunk) => {
|
|
165
|
+
out.write(chunk);
|
|
166
|
+
pending += chunk.toString("utf8");
|
|
167
|
+
const lines = pending.split("\n");
|
|
168
|
+
pending = lines.pop() ?? "";
|
|
169
|
+
for (const line of lines) {
|
|
170
|
+
options.onLine?.(line);
|
|
171
|
+
if (line.trim()) {
|
|
172
|
+
tail.push(line.length > 200 ? `${line.slice(0, 199)}…` : line);
|
|
173
|
+
if (tail.length > 8)
|
|
174
|
+
tail.shift();
|
|
175
|
+
}
|
|
176
|
+
const issue = parseIssue(line);
|
|
177
|
+
if (!issue)
|
|
178
|
+
continue;
|
|
179
|
+
// The same diagnostic repeats (once per architecture or compile batch).
|
|
180
|
+
const key = `${issue.file}:${issue.line}:${issue.severity}:${issue.message}`;
|
|
181
|
+
if (seen.has(key))
|
|
182
|
+
continue;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
if (issue.severity === "warning")
|
|
185
|
+
warnings++;
|
|
186
|
+
else
|
|
187
|
+
errors.push(issue);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
child.stdout.on("data", take);
|
|
191
|
+
child.stderr.on("data", take);
|
|
192
|
+
const code = await new Promise((resolveCode) => child.on("close", resolveCode));
|
|
193
|
+
out.end();
|
|
194
|
+
return { ok: code === 0, errors, warnings, log, ms: Date.now() - started, tail };
|
|
195
|
+
}
|
|
196
|
+
/** The .app a Simulator build made: the scheme's own, never a test runner. */
|
|
197
|
+
export function builtApp(project, scheme, configuration = "Debug") {
|
|
198
|
+
const products = join(derivedDataFor(project), "Build", "Products", `${configuration}-iphonesimulator`);
|
|
199
|
+
if (!existsSync(products))
|
|
200
|
+
return undefined;
|
|
201
|
+
const apps = readdirSync(products).filter((e) => e.endsWith(".app") && !e.endsWith("-Runner.app"));
|
|
202
|
+
const named = apps.find((a) => a === `${scheme}.app`);
|
|
203
|
+
return named || apps.length === 1 ? join(products, named ?? apps[0]) : undefined;
|
|
204
|
+
}
|
|
205
|
+
const memoryFile = async (dir) => {
|
|
206
|
+
const folder = join(stateDir(), "projects");
|
|
207
|
+
await mkdir(folder, { recursive: true });
|
|
208
|
+
return join(folder, `${createHash("sha1").update(resolve(dir)).digest("hex").slice(0, 16)}.json`);
|
|
209
|
+
};
|
|
210
|
+
export async function remembered(dir) {
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(await readFile(await memoryFile(dir), "utf8"));
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export async function remember(dir, value) {
|
|
219
|
+
await writeFile(await memoryFile(dir), JSON.stringify(value));
|
|
220
|
+
}
|
|
221
|
+
/** The project the last app_run or tests_run used, for calls that name none from a folder without one. */
|
|
222
|
+
export async function lastProject() {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(await readFile(join(stateDir(), "last-project.json"), "utf8")).project;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export async function setLastProject(project) {
|
|
231
|
+
await mkdir(stateDir(), { recursive: true });
|
|
232
|
+
await writeFile(join(stateDir(), "last-project.json"), JSON.stringify({ project }));
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Which project a call means: the path given; else the current folder's (or what app_run remembered for it);
|
|
236
|
+
* else the one the last call used, so an agent that named a path once can drop it.
|
|
237
|
+
*/
|
|
238
|
+
export async function projectFor(path, scheme) {
|
|
239
|
+
const dir = path ?? process.cwd();
|
|
240
|
+
const memory = await remembered(dir);
|
|
241
|
+
let project;
|
|
242
|
+
let note = "";
|
|
243
|
+
try {
|
|
244
|
+
project = await findProject(memory && !path ? memory.project : dir);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const last = path ? undefined : await lastProject();
|
|
248
|
+
if (!last || !existsSync(last))
|
|
249
|
+
throw error;
|
|
250
|
+
project = await findProject(last);
|
|
251
|
+
note = `No project in ${dir}; using ${project.name} from the last call (${project.dir}).\n`;
|
|
252
|
+
}
|
|
253
|
+
if (project.note)
|
|
254
|
+
note += `${project.note}\n`;
|
|
255
|
+
const rememberedScheme = memory?.project === project.path ? memory.scheme : (await remembered(project.dir))?.scheme;
|
|
256
|
+
const chosen = await pickScheme(project, scheme ?? rememberedScheme);
|
|
257
|
+
await setLastProject(project.path);
|
|
258
|
+
return { project, scheme: chosen, dir: path ? dir : project.dir, note };
|
|
259
|
+
}
|