sim-mirror 1.2.0__tar.gz → 2.0.0__tar.gz
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.
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/PKG-INFO +25 -11
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/README.md +24 -10
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/THIRD_PARTY_LICENSES.md +43 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/docs/README.md +6 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/docs/media/README.md +6 -5
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/claude-code-cli/README.md +2 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/codex/README.md +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/cursor/README.md +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/fastapi-embed/README.md +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Package.swift +9 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Arguments.swift +26 -1
- sim_mirror-2.0.0/helper/Sources/HelperCore/Capture.swift +202 -0
- sim_mirror-2.0.0/helper/Sources/HelperCore/Recording.swift +247 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Requests.swift +7 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Server.swift +10 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Video.swift +25 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Wire.swift +1 -1
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/CaptureDevice.swift +187 -0
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/Disclaimed.swift +46 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Framebuffer.swift +18 -40
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/H264Stream.swift +36 -29
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/PixelSource.swift +91 -0
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/RecordingRenderer.swift +362 -0
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/ScreenFeed.swift +60 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/SimulatorDevice.swift +24 -48
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/UsbCapture.swift +245 -0
- sim_mirror-2.0.0/helper/Sources/HelperPlatform/Warming.swift +29 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/sim-mirror-helper/main.swift +74 -31
- sim_mirror-2.0.0/helper/Support/Info.plist +12 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/ArgumentsTests.swift +5 -1
- sim_mirror-2.0.0/helper/Tests/HelperCoreTests/CaptureTests.swift +108 -0
- sim_mirror-2.0.0/helper/Tests/HelperCoreTests/RecordingTests.swift +139 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/ServerTests.swift +5 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/WireTests.swift +1 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/README.md +2 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/common.schema.json +40 -3
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/http.schema.json +56 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/status.schema.json +23 -4
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/pyproject.toml +1 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/scripts/hatch_build.py +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/_version.py +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/api.py +1 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/destination.py +4 -4
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/provider.py +15 -5
- sim_mirror-2.0.0/src/sim_mirror/build/wda.py +147 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/xcodebuild.py +62 -11
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/context.py +3 -0
- sim_mirror-2.0.0/src/sim_mirror/cli/device_command.py +58 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/devices.py +10 -6
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/main.py +5 -2
- sim_mirror-2.0.0/src/sim_mirror/cli/record.py +45 -0
- sim_mirror-2.0.0/src/sim_mirror/cli/wda.py +176 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/model.py +23 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/schema.py +114 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/base.py +38 -2
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/helper_process.py +35 -10
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/__init__.py +2 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/cable.py +39 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/capture.py +140 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/connector.py +276 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/screen.py +78 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda.py +208 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_client.py +190 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_roles.py +316 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_setup.py +178 -0
- sim_mirror-2.0.0/src/sim_mirror/connectors/iphone/wda_source.py +246 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/client.py +3 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/connector.py +3 -18
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/helper.py +115 -1
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/registry.py +56 -8
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/capture.py +2 -15
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/connector.py +4 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/actions.py +24 -4
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/availability.py +4 -2
- sim_mirror-2.0.0/src/sim_mirror/core/backends.py +204 -0
- sim_mirror-2.0.0/src/sim_mirror/core/control.py +317 -0
- sim_mirror-2.0.0/src/sim_mirror/core/device_changes.py +217 -0
- sim_mirror-2.0.0/src/sim_mirror/core/device_logs.py +138 -0
- sim_mirror-2.0.0/src/sim_mirror/core/device_settings.py +111 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/devices.py +4 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/instance.py +35 -3
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/manager.py +207 -60
- sim_mirror-2.0.0/src/sim_mirror/core/recording_sources.py +168 -0
- sim_mirror-2.0.0/src/sim_mirror/core/recordings.py +509 -0
- sim_mirror-2.0.0/src/sim_mirror/core/render.py +79 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/runtime.py +77 -6
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/screen_input.py +15 -8
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/screen_relay.py +6 -4
- sim_mirror-2.0.0/src/sim_mirror/core/signing.py +92 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/status.py +16 -11
- sim_mirror-2.0.0/src/sim_mirror/core/touch.py +99 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/checks.py +5 -1
- sim_mirror-2.0.0/src/sim_mirror/doctor/real_devices.py +132 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/tap.py +3 -3
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/host_copy.py +111 -0
- sim_mirror-2.0.0/src/sim_mirror/platform/devicectl.py +399 -0
- sim_mirror-2.0.0/src/sim_mirror/platform/errors.py +15 -0
- sim_mirror-2.0.0/src/sim_mirror/platform/identifiers.py +41 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/images.py +13 -3
- sim_mirror-2.0.0/src/sim_mirror/platform/keychain.py +79 -0
- sim_mirror-2.0.0/src/sim_mirror/platform/lockdown.py +192 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/simctl.py +109 -10
- sim_mirror-2.0.0/src/sim_mirror/platform/usbmux.py +171 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/_generated.py +146 -3
- sim_mirror-2.0.0/src/sim_mirror/server/http_routes.py +200 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/settings_routes.py +1 -1
- sim_mirror-2.0.0/src/sim_mirror/server/static/viewer/start.js +32 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/daemon.py +90 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fakes.py +196 -6
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/development-certificate.pem +24 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-appearance.json +46 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-apps.json +59 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-devices.json +1065 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-displays.json +63 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-launch-missing.json +58 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-launch.json +53 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-lock-state.json +27 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-processes.json +39 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/fixtures/devicectl-screenshot.json +31 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/guards.py +9 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/native.py +10 -1
- sim_mirror-2.0.0/src/sim_mirror/testing/recording.py +64 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/rig.py +29 -16
- sim_mirror-2.0.0/src/sim_mirror/testing/usbmux.py +214 -0
- sim_mirror-2.0.0/src/sim_mirror/testing/wda.py +252 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/app.py +22 -19
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/context.py +16 -5
- sim_mirror-2.0.0/src/sim_mirror/tools/device.py +128 -0
- sim_mirror-2.0.0/src/sim_mirror/tools/record.py +88 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/registry.py +4 -4
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/schemas.py +55 -5
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/tests/unit/protocol/test_messages.py +4 -0
- sim_mirror-1.2.0/src/sim_mirror/server/http_routes.py +0 -93
- sim_mirror-1.2.0/src/sim_mirror/server/static/viewer/start.js +0 -26
- sim_mirror-1.2.0/src/sim_mirror/tools/device.py +0 -51
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/.gitignore +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/LICENSE +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/NOTICE +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/custom-connector/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/embed-host/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/iframe/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/token-budget/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/examples/web-component/README.md +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Accessibility.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/FrameScheduler.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Input.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/JSON.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperCore/Screen.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/AccessibilityReader.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Frameworks.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/HIDTransports.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/Pictures.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/SimDeviceHandle.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/HelperPlatform/UnixSocket.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/ObjCGuard/ObjCGuard.m +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Sources/ObjCGuard/include/ObjCGuard.h +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/Fixtures/wire-vectors.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/AccessibilityTests.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/InputTests.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/helper/Tests/HelperCoreTests/PictureTests.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/error-inactive.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/error-unauthorized.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-alert.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-keyboard.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-swiftui.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-tagged.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/hierarchy-uikit.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/examples/listing.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/hierarchy.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/app-sdk/v1/listing.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/agent-event.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/client-input.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/close-codes.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/constants.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/hello.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/screen-text.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/settings.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/protocol/v1/stream.schema.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/__main__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/build/xcresult.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/app_command.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/config_command.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/doctor.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/helper.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/mcp.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/open_viewer.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/serve.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/settings_command.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/tokens.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/tools.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/version.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/cli/xcode.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/discovery.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/env.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/provenance.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/settings_store.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/toml_source.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/config/writer.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/client.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/discovery.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/document.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/errors.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/merge.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/reader.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/app/wire.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/companion.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/connector.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/engine.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/frameworks.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb.proto +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb_grpc.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/idb/proto/idb_pb2.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/client.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/connector.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/hierarchy.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/merge.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/mcpbridge/reader.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/native/wire.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/connectors/simctl/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/events.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/frames.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/gestures.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/reaper.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/text_entry.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/text_overlay.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/core/tickets.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/app.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/auth.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/confirmations.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/health.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/lease.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/lifecycle.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/passes.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/policy.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/daemon/tokens.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/macos.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/doctor/report.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/hosting.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/launcher.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/mcp/relay.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/budget.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/imagehash.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/model.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/ocr.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/readers.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/settle.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/snapshot.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/helper.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/probe.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/vision/text_recognizer.swift +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/perception/wait.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/developer_dir.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/device_data.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/json_lines.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/keyboard.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/process.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/swift.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/swiftpm.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/xcode.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/platform/xcrun.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/protocol/messages.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/py.typed +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/scope.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/seams.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/agent_routes.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/envelope.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/log_redaction.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/pages.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/security.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/settings_view.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/socket_routes.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/static/viewer/embed.html +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/server/static/viewer/index.html +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/app_support.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/claims.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/storage/private.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/app_sdk.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/asgi.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/contract.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-alert-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-alert.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-keyboard.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-swiftui.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-tagged.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-app-sdk-uikit.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-home-complete.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-home-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-keyboard-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-probe-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-safari-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-safari-typed.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-settings-interactable.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/ax-settings-ios27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-answers.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-alert.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-home.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-keyboard.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-probe.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-safari.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/mcpbridge-hierarchy-settings.txt +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/simctl-devices.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/simctl-runtimes.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-list-names.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-list.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-settings.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-test-plans-none.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcodebuild-test-plans.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-broken.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-ok.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-build-results-unbuilt.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-retries.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing-xcode27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-swift-testing.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary-unbuilt.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-summary.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-retries.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing-xcode27.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests-swift-testing.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/fixtures/xcresult-test-tests.json +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/pictures.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/testing/vision.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/__init__.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/act.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/results.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/screenshot.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/tools/snapshot.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/src/sim_mirror/validation.py +0 -0
- {sim_mirror-1.2.0 → sim_mirror-2.0.0}/viewer/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sim-mirror
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Mirror and drive the iOS Simulator from Claude Code, CLI agents and the browser.
|
|
5
5
|
Project-URL: Homepage, https://github.com/AndrewKochulab/sim-mirror
|
|
6
6
|
Project-URL: Documentation, https://github.com/AndrewKochulab/sim-mirror/tree/main/docs
|
|
@@ -45,16 +45,21 @@ Description-Content-Type: text/markdown
|
|
|
45
45
|
[](LICENSE)
|
|
46
46
|
[](https://github.com/AndrewKochulab/sim-mirror/releases)
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
<img src="docs/media/hero.gif" width="100%"
|
|
49
|
+
alt="A Claude Code session driving a calendar app on an iPhone 17 simulator in the viewer: it opens an event, scrolls
|
|
50
|
+
through its notes, then fills in and saves a new one, the agent's cursor gliding to each spot before it taps">
|
|
51
|
+
|
|
52
|
+
*A real session, sped up four times. [The whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4)
|
|
53
|
+
runs 1m 39s.*
|
|
50
54
|
|
|
51
55
|
A live iOS Simulator in any browser tab or web page, an animated cursor that shows exactly what your AI agent is about
|
|
52
56
|
to tap, and token-efficient UI snapshots so agents read the screen as compact text instead of screenshots. Works with
|
|
53
57
|
Claude Code, Codex, Cursor and any other MCP client.
|
|
54
58
|
|
|
55
|
-
> **Status:
|
|
56
|
-
>
|
|
57
|
-
>
|
|
59
|
+
> **Status: 2.0.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
|
|
60
|
+
> release to [what SimMirror promises not to break](docs/stability.md). 2.0 brings real iPhones and iPads beside the
|
|
61
|
+
> simulators -- live over the cable, driven through WebDriverAgent -- and records any screen as an MP4 or a GIF. It
|
|
62
|
+
> removes nothing 1.x promised.
|
|
58
63
|
|
|
59
64
|
## Why SimMirror
|
|
60
65
|
|
|
@@ -79,7 +84,7 @@ Claude Code, Codex, Cursor and any other MCP client.
|
|
|
79
84
|
**Any MCP client**, as a stdio server:
|
|
80
85
|
|
|
81
86
|
```sh
|
|
82
|
-
claude mcp add sim-mirror -- uvx --from sim-mirror==
|
|
87
|
+
claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
|
|
83
88
|
```
|
|
84
89
|
|
|
85
90
|
Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.md) and
|
|
@@ -88,7 +93,7 @@ Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.
|
|
|
88
93
|
**The command**, for `sim-mirror open`, `doctor` and the rest:
|
|
89
94
|
|
|
90
95
|
```sh
|
|
91
|
-
uv tool install sim-mirror==
|
|
96
|
+
uv tool install sim-mirror==2.0.0
|
|
92
97
|
# or
|
|
93
98
|
brew install andrewkochulab/tap/sim-mirror
|
|
94
99
|
```
|
|
@@ -138,6 +143,14 @@ in a Debug build on the simulator it shares its own UIKit and SwiftUI views, so
|
|
|
138
143
|
gesture or a hand-drawn control reads by name, and `.simMirror("Settings")` names a SwiftUI view. Optional, and nothing
|
|
139
144
|
of it is in a Release build. See [The app SDK](docs/app-sdk.md).
|
|
140
145
|
|
|
146
|
+
**Real devices.** A cabled iPhone or iPad shows up beside the simulators: its live screen over the cable, its apps,
|
|
147
|
+
logs, look and location through Xcode's `devicectl`, and touching, typing and its element tree through a WebDriverAgent
|
|
148
|
+
you build with your own team. What SimMirror changes on it is put back when it is let go. See
|
|
149
|
+
[Real devices](docs/real-devices.md).
|
|
150
|
+
|
|
151
|
+
**Recording.** The viewer's Record button, `sim_record` or `sim-mirror record` keep a device's screen as an MP4, a GIF
|
|
152
|
+
or both, with each touch drawn where it landed and a 9:41 status bar. See [Recording](docs/recording.md).
|
|
153
|
+
|
|
141
154
|
**Connectors.** SimMirror's own native helper gives full control with nothing to install; idb_companion still can,
|
|
142
155
|
and the simctl connector mirrors the screen view-only when neither is there, and says why. `auto` uses the fastest that
|
|
143
156
|
works and falls back when one fails. More connectors plug in through an entry point. See
|
|
@@ -185,6 +198,7 @@ an image at about 750 pixels a token).
|
|
|
185
198
|
| macOS | 26 (15 with Xcode 16.4, view-only) |
|
|
186
199
|
| Xcode | 26 and 27 |
|
|
187
200
|
| Connectors | native helper (full control), idb_companion 1.5 (full control), simctl (view-only) |
|
|
201
|
+
| Real devices | an iPhone or iPad that Xcode's `devicectl` reaches, by cable (view-only without WebDriverAgent) |
|
|
188
202
|
| App SDK | Debug builds on the iOS Simulator, iOS 16 or later |
|
|
189
203
|
| Clients | Claude Code, Codex, Cursor, any stdio MCP client |
|
|
190
204
|
| Browsers | Chrome, Safari, Firefox, Edge |
|
|
@@ -192,7 +206,7 @@ an image at about 750 pixels a token).
|
|
|
192
206
|
Rows checked on real hardware are marked verified, with the date, in [Compatibility](docs/compatibility.md).
|
|
193
207
|
|
|
194
208
|
From 1.0 the tools, the protocol, the embedding API, the viewer package, settings and the command line are held to
|
|
195
|
-
[what SimMirror promises not to break](docs/stability.md): a
|
|
209
|
+
[what SimMirror promises not to break](docs/stability.md): a release adds, and never breaks; 2.0 broke nothing. Connectors are the
|
|
196
210
|
exception, and pin a minor version.
|
|
197
211
|
|
|
198
212
|
## Documentation
|
|
@@ -208,8 +222,8 @@ the [command line](docs/reference/cli.md) and the [protocol](docs/reference/prot
|
|
|
208
222
|
| **v0.1** (preview) | Live viewer, agent cursor, person and agent control, MCP tools, `sim-mirror doctor`, idb and simctl connectors, build and test as a preview |
|
|
209
223
|
| **v0.2** | Build, run and test tools, a settings panel, Xcode 27's UI hierarchy through `mcpbridge`, one shared daemon for many hosts |
|
|
210
224
|
| **v1.0** | Stable protocol and embedding API, held by a check; PyPI, npm and the MCP Registry; a cursor that stays while the agent works |
|
|
211
|
-
| **v1.2**
|
|
212
|
-
| **v2.0** | Real iPhones:
|
|
225
|
+
| **v1.2** | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
|
|
226
|
+
| **v2.0** (now) | Real iPhones and iPads: the live screen over the cable, apps, logs, look and location without signing; full control through WebDriverAgent; recording a screen as an MP4 or GIF |
|
|
213
227
|
| **Android** | Android emulator support with the same viewer, cursor and tools |
|
|
214
228
|
| **Website & launch** | Landing page, video tutorials and guides |
|
|
215
229
|
|
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://github.com/AndrewKochulab/sim-mirror/releases)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
<img src="docs/media/hero.gif" width="100%"
|
|
12
|
+
alt="A Claude Code session driving a calendar app on an iPhone 17 simulator in the viewer: it opens an event, scrolls
|
|
13
|
+
through its notes, then fills in and saves a new one, the agent's cursor gliding to each spot before it taps">
|
|
14
|
+
|
|
15
|
+
*A real session, sped up four times. [The whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4)
|
|
16
|
+
runs 1m 39s.*
|
|
13
17
|
|
|
14
18
|
A live iOS Simulator in any browser tab or web page, an animated cursor that shows exactly what your AI agent is about
|
|
15
19
|
to tap, and token-efficient UI snapshots so agents read the screen as compact text instead of screenshots. Works with
|
|
16
20
|
Claude Code, Codex, Cursor and any other MCP client.
|
|
17
21
|
|
|
18
|
-
> **Status:
|
|
19
|
-
>
|
|
20
|
-
>
|
|
22
|
+
> **Status: 2.0.** The agent tools, the viewer, the protocol and the embedding API are stable, and a check holds every
|
|
23
|
+
> release to [what SimMirror promises not to break](docs/stability.md). 2.0 brings real iPhones and iPads beside the
|
|
24
|
+
> simulators -- live over the cable, driven through WebDriverAgent -- and records any screen as an MP4 or a GIF. It
|
|
25
|
+
> removes nothing 1.x promised.
|
|
21
26
|
|
|
22
27
|
## Why SimMirror
|
|
23
28
|
|
|
@@ -42,7 +47,7 @@ Claude Code, Codex, Cursor and any other MCP client.
|
|
|
42
47
|
**Any MCP client**, as a stdio server:
|
|
43
48
|
|
|
44
49
|
```sh
|
|
45
|
-
claude mcp add sim-mirror -- uvx --from sim-mirror==
|
|
50
|
+
claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
|
|
46
51
|
```
|
|
47
52
|
|
|
48
53
|
Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.md) and
|
|
@@ -51,7 +56,7 @@ Configurations for [Codex](docs/clients/codex.md), [Cursor](docs/clients/cursor.
|
|
|
51
56
|
**The command**, for `sim-mirror open`, `doctor` and the rest:
|
|
52
57
|
|
|
53
58
|
```sh
|
|
54
|
-
uv tool install sim-mirror==
|
|
59
|
+
uv tool install sim-mirror==2.0.0
|
|
55
60
|
# or
|
|
56
61
|
brew install andrewkochulab/tap/sim-mirror
|
|
57
62
|
```
|
|
@@ -101,6 +106,14 @@ in a Debug build on the simulator it shares its own UIKit and SwiftUI views, so
|
|
|
101
106
|
gesture or a hand-drawn control reads by name, and `.simMirror("Settings")` names a SwiftUI view. Optional, and nothing
|
|
102
107
|
of it is in a Release build. See [The app SDK](docs/app-sdk.md).
|
|
103
108
|
|
|
109
|
+
**Real devices.** A cabled iPhone or iPad shows up beside the simulators: its live screen over the cable, its apps,
|
|
110
|
+
logs, look and location through Xcode's `devicectl`, and touching, typing and its element tree through a WebDriverAgent
|
|
111
|
+
you build with your own team. What SimMirror changes on it is put back when it is let go. See
|
|
112
|
+
[Real devices](docs/real-devices.md).
|
|
113
|
+
|
|
114
|
+
**Recording.** The viewer's Record button, `sim_record` or `sim-mirror record` keep a device's screen as an MP4, a GIF
|
|
115
|
+
or both, with each touch drawn where it landed and a 9:41 status bar. See [Recording](docs/recording.md).
|
|
116
|
+
|
|
104
117
|
**Connectors.** SimMirror's own native helper gives full control with nothing to install; idb_companion still can,
|
|
105
118
|
and the simctl connector mirrors the screen view-only when neither is there, and says why. `auto` uses the fastest that
|
|
106
119
|
works and falls back when one fails. More connectors plug in through an entry point. See
|
|
@@ -148,6 +161,7 @@ an image at about 750 pixels a token).
|
|
|
148
161
|
| macOS | 26 (15 with Xcode 16.4, view-only) |
|
|
149
162
|
| Xcode | 26 and 27 |
|
|
150
163
|
| Connectors | native helper (full control), idb_companion 1.5 (full control), simctl (view-only) |
|
|
164
|
+
| Real devices | an iPhone or iPad that Xcode's `devicectl` reaches, by cable (view-only without WebDriverAgent) |
|
|
151
165
|
| App SDK | Debug builds on the iOS Simulator, iOS 16 or later |
|
|
152
166
|
| Clients | Claude Code, Codex, Cursor, any stdio MCP client |
|
|
153
167
|
| Browsers | Chrome, Safari, Firefox, Edge |
|
|
@@ -155,7 +169,7 @@ an image at about 750 pixels a token).
|
|
|
155
169
|
Rows checked on real hardware are marked verified, with the date, in [Compatibility](docs/compatibility.md).
|
|
156
170
|
|
|
157
171
|
From 1.0 the tools, the protocol, the embedding API, the viewer package, settings and the command line are held to
|
|
158
|
-
[what SimMirror promises not to break](docs/stability.md): a
|
|
172
|
+
[what SimMirror promises not to break](docs/stability.md): a release adds, and never breaks; 2.0 broke nothing. Connectors are the
|
|
159
173
|
exception, and pin a minor version.
|
|
160
174
|
|
|
161
175
|
## Documentation
|
|
@@ -171,8 +185,8 @@ the [command line](docs/reference/cli.md) and the [protocol](docs/reference/prot
|
|
|
171
185
|
| **v0.1** (preview) | Live viewer, agent cursor, person and agent control, MCP tools, `sim-mirror doctor`, idb and simctl connectors, build and test as a preview |
|
|
172
186
|
| **v0.2** | Build, run and test tools, a settings panel, Xcode 27's UI hierarchy through `mcpbridge`, one shared daemon for many hosts |
|
|
173
187
|
| **v1.0** | Stable protocol and embedding API, held by a check; PyPI, npm and the MCP Registry; a cursor that stays while the agent works |
|
|
174
|
-
| **v1.2**
|
|
175
|
-
| **v2.0** | Real iPhones:
|
|
188
|
+
| **v1.2** | SimMirror's own native helper instead of idb_companion, screens read from their pixels and settling past endless animations, an optional in-app debug SDK, and a Homebrew tap |
|
|
189
|
+
| **v2.0** (now) | Real iPhones and iPads: the live screen over the cable, apps, logs, look and location without signing; full control through WebDriverAgent; recording a screen as an MP4 or GIF |
|
|
176
190
|
| **Android** | Android emulator support with the same viewer, cursor and tools |
|
|
177
191
|
| **Website & launch** | Landing page, video tutorials and guides |
|
|
178
192
|
|
|
@@ -84,5 +84,48 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
84
84
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
## WebDriverAgent
|
|
88
|
+
|
|
89
|
+
- **What:** the XCTest runner SimMirror uses to touch, type on and read a real device
|
|
90
|
+
([Real devices](docs/real-devices.md#tier-2-webdriveragent)). It is **not** shipped: `sim-mirror wda setup` fetches
|
|
91
|
+
release 16.12.10 (commit `00c38220c3e84906c965b996ffc4c12d09fef62f`), checks its SHA-256, makes two changes -- its
|
|
92
|
+
screen stream is bound to the device's loopback, as its API is, and a point of a touch is used where it is given
|
|
93
|
+
rather than looked up against the app -- and builds it on your Mac with your team.
|
|
94
|
+
- **Source:** [appium/WebDriverAgent](https://github.com/appium/WebDriverAgent), first written by Facebook.
|
|
95
|
+
- **License:** BSD-3-Clause
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
BSD License
|
|
99
|
+
|
|
100
|
+
For WebDriverAgent software
|
|
101
|
+
|
|
102
|
+
Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
|
|
103
|
+
|
|
104
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
105
|
+
are permitted provided that the following conditions are met:
|
|
106
|
+
|
|
107
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
108
|
+
list of conditions and the following disclaimer.
|
|
109
|
+
|
|
110
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
111
|
+
this list of conditions and the following disclaimer in the documentation
|
|
112
|
+
and/or other materials provided with the distribution.
|
|
113
|
+
|
|
114
|
+
* Neither the name Facebook nor the names of its contributors may be used to
|
|
115
|
+
endorse or promote products derived from this software without specific
|
|
116
|
+
prior written permission.
|
|
117
|
+
|
|
118
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
119
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
120
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
121
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
122
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
123
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
124
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
125
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
126
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
127
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
128
|
+
```
|
|
129
|
+
|
|
87
130
|
Runtime dependencies installed from PyPI and npm (FastAPI, Uvicorn, grpclib, protobuf, tomlkit, Pillow and others) are
|
|
88
131
|
not redistributed here; each carries its own license in its package.
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
- [Cursor](clients/cursor.md)
|
|
14
14
|
- [Any other MCP client](clients/other-mcp.md)
|
|
15
15
|
|
|
16
|
+
## Use it with a real device, and record
|
|
17
|
+
|
|
18
|
+
- [Real devices](real-devices.md) -- a cabled iPhone or iPad: its live screen, apps and logs, and WebDriverAgent for
|
|
19
|
+
touch, typing and the element tree
|
|
20
|
+
- [Recording](recording.md) -- a screen as an MP4 or a GIF, with each touch drawn, and a demo in a pull request
|
|
21
|
+
|
|
16
22
|
## Change its settings
|
|
17
23
|
|
|
18
24
|
- [The settings panel](settings.md) -- every setting from the viewer, and a code at the terminal for the ones a page
|
|
@@ -4,17 +4,18 @@ Pictures the README and the docs show. Every clone carries every picture ever co
|
|
|
4
4
|
keeps them small: a GIF at most 3 MB, a still image at most 400 KB, the folder at most 25 MB. Videos are attached to a
|
|
5
5
|
GitHub Release instead.
|
|
6
6
|
|
|
7
|
-
Captured from real runs, never mocked up.
|
|
8
|
-
|
|
7
|
+
Captured from real runs, never mocked up. Most were taken on 2026-09-16 from one machine -- macOS 26.6.2, Xcode 26.6,
|
|
8
|
+
an iPhone 17 Pro simulator on iOS 26.5 -- with SimMirror 0.1.0; a row says so when its picture came from a later run:
|
|
9
9
|
|
|
10
10
|
| File | Shows | How it was made |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| `hero.gif` | A Claude Code session
|
|
13
|
-
| `cursor.gif` | The agent cursor gliding to a row and the screen following | The
|
|
12
|
+
| `hero.gif` | A Claude Code session reading and writing calendar events, its cursor drawn before each tap | A screen recording made later, on 2026-09-17 with SimMirror 1.2.0, Xcode 27.0 and an iPhone 17 simulator on iOS 27.0, of a real Claude Code session driving a calendar app over MCP -- which is why the cursor's chip reads `claude-code` and the device says it is in use. Sped up four times and cut to 700 px wide to stay under the GIF limit; [the whole recording](https://github.com/AndrewKochulab/sim-mirror/releases/download/v1.2.0/sim-mirror-plugin-demo.mp4) is attached to the 1.2.0 release |
|
|
13
|
+
| `cursor.gif` | The agent cursor gliding to a row and the screen following | The 2026-09-16 Settings run, the moment around one tap |
|
|
14
14
|
| `viewer.png` | The viewer in a browser tab | A screenshot of the tab, live |
|
|
15
15
|
| `snapshot-vs-screenshot.png` | A snapshot beside a screenshot of the same screen | The real answers of `sim_screenshot` and `sim_snapshot`, taken a moment apart, set side by side with their measured token estimates |
|
|
16
16
|
| `iframe.png` | The viewer framed in another page | The `examples/iframe` page on its own origin, with a one-shot embed ticket |
|
|
17
17
|
| `view-only.png` | The view-only mirror without idb_companion | The same viewer with `connectors.idb.companion_path` pointed at nothing, so the simctl connector serves it |
|
|
18
18
|
| `doctor.png` | `sim-mirror doctor` on a Mac | The real output of `sim-mirror doctor --no-tap`, typeset for the docs rather than photographed from a terminal |
|
|
19
|
-
| `social-preview.png` | GitHub's social preview, 1280×640 | The project's name and claims beside a frame of `hero.gif` |
|
|
19
|
+
| `social-preview.png` | GitHub's social preview, 1280×640 | The project's name and claims beside a frame of the Settings run `hero.gif` showed until 1.2.0 |
|
|
20
|
+
| `recording-demo.gif` | A recording an agent made: Settings › Appearance switched to dark and back, its taps drawn | `sim_record` itself, on 2026-09-29 with Xcode 27.0 and an iPhone 18 Pro simulator on iOS 27.0: an agent started a GIF recording with the demo status bar, tapped through Settings with `sim_act`, and stopped it; the file is as `sim_record stop` kept it, 600 px wide |
|
|
20
21
|
| `settings-panel.png` | The settings panel's Server tab | A screenshot of the panel in Chrome, taken later on 2026-09-16 with Xcode 27.0 and an iOS 27.0 simulator, from a daemon started with `SIM_MIRROR_SERVER_PORT=7491` -- which is why the port shows as locked |
|
|
@@ -16,7 +16,7 @@ Its tools are named `mcp__plugin_sim-mirror_sim-mirror__sim_snapshot` and so on.
|
|
|
16
16
|
## Without the plugin
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
claude mcp add sim-mirror -- uvx --from sim-mirror==
|
|
19
|
+
claude mcp add sim-mirror -- uvx --from sim-mirror==2.0.0 sim-mirror mcp
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Add `--scope project` before the name to write it to the project's `.mcp.json` for everyone working on it. Its tools
|
|
@@ -30,7 +30,7 @@ simulator (its scope) and this folder as the only place builds and installs may
|
|
|
30
30
|
## Watch it
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
uvx --from sim-mirror==
|
|
33
|
+
uvx --from sim-mirror==2.0.0 sim-mirror open
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
opens this folder's viewer in your browser. Every gesture the agent makes is drawn there as a cursor just before it
|
|
@@ -6,7 +6,7 @@ Add [config.toml](config.toml)'s table to `~/.codex/config.toml`, then start Cod
|
|
|
6
6
|
and installs may reach. The commented `args` pin a project by name and path instead.
|
|
7
7
|
- `tool_timeout_sec` is raised because `sim_build_run` and `sim_test` can take minutes (they are there only when build
|
|
8
8
|
tools are switched on).
|
|
9
|
-
- Watch and take over in a browser: `uvx --from sim-mirror==
|
|
9
|
+
- Watch and take over in a browser: `uvx --from sim-mirror==2.0.0 sim-mirror open`
|
|
10
10
|
in the same folder.
|
|
11
11
|
|
|
12
12
|
The tools are the same for every client; see [claude-code-cli](../claude-code-cli/) for a first prompt to try.
|
|
@@ -6,7 +6,7 @@ one), and enable the server in Cursor's MCP settings.
|
|
|
6
6
|
- `sim-mirror mcp` gives the agent the simulator of the folder it starts in, and that folder as the only place builds
|
|
7
7
|
and installs may reach. If the tools act on the wrong project, append `"--scope", "my-app", "--root",
|
|
8
8
|
"/absolute/path/to/my-app"` to `args`.
|
|
9
|
-
- Watch and take over in a browser: `uvx --from sim-mirror==
|
|
9
|
+
- Watch and take over in a browser: `uvx --from sim-mirror==2.0.0 sim-mirror open`
|
|
10
10
|
in the same folder.
|
|
11
11
|
|
|
12
12
|
The tools are the same for every client; see [claude-code-cli](../claude-code-cli/) for a first prompt to try.
|
|
@@ -5,7 +5,7 @@ A Python application with SimMirror inside it: [app.py](app.py). No daemon runs;
|
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
7
|
cd examples/fastapi-embed
|
|
8
|
-
EXAMPLE_KEY=change-me uv run --with "sim-mirror==
|
|
8
|
+
EXAMPLE_KEY=change-me uv run --with "sim-mirror==2.0.0" \
|
|
9
9
|
uvicorn --factory app:create_app --port 7484
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -28,7 +28,15 @@ let package = Package(
|
|
|
28
28
|
.executableTarget(
|
|
29
29
|
name: "sim-mirror-helper",
|
|
30
30
|
dependencies: ["HelperCore", "HelperPlatform"],
|
|
31
|
-
swiftSettings: [.swiftLanguageMode(.v5)]
|
|
31
|
+
swiftSettings: [.swiftLanguageMode(.v5)],
|
|
32
|
+
// The Info.plist macOS reads to ask for the Camera, which reading a cabled device's screen needs, embedded
|
|
33
|
+
// in the program itself.
|
|
34
|
+
linkerSettings: [
|
|
35
|
+
.unsafeFlags([
|
|
36
|
+
"-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist",
|
|
37
|
+
"-Xlinker", "\(Context.packageDirectory)/Support/Info.plist",
|
|
38
|
+
]),
|
|
39
|
+
]
|
|
32
40
|
),
|
|
33
41
|
.testTarget(name: "HelperCoreTests", dependencies: ["HelperCore"]),
|
|
34
42
|
]
|
|
@@ -10,12 +10,21 @@ public enum Command: Equatable, Sendable {
|
|
|
10
10
|
/// Reach a device the way serving would -- its screen, a picture, input and the element tree -- say what worked as
|
|
11
11
|
/// JSON, and exit 0 only if all of it did.
|
|
12
12
|
case selfCheck(DeviceOptions)
|
|
13
|
+
/// Render a recording as the job at a path asks -- an MP4, a GIF, its touches drawn in -- say what it wrote as JSON,
|
|
14
|
+
/// and exit.
|
|
15
|
+
case render(job: String)
|
|
16
|
+
/// Serve a cabled real device's screen, read through AVFoundation, on a unix socket until SimMirror lets go of it.
|
|
17
|
+
case capture(CaptureOptions)
|
|
13
18
|
|
|
14
19
|
public static let usage = """
|
|
15
20
|
usage: sim-mirror-helper version
|
|
16
21
|
sim-mirror-helper serve --udid UDID --socket PATH [--parent-pid PID] [--hid auto|dtuhid|indigo]
|
|
17
22
|
[--idle-key-frames on|off] [--log-level debug|info|warning|error]
|
|
18
23
|
sim-mirror-helper self-check --udid UDID [--hid auto|dtuhid|indigo]
|
|
24
|
+
sim-mirror-helper render --job PATH
|
|
25
|
+
sim-mirror-helper capture --udid UDID --name NAME --socket PATH --width-px N --height-px N --scale N
|
|
26
|
+
[--capture-id ID] [--reference PATH] [--parent-pid PID] [--wait SECONDS] [--linger SECONDS]
|
|
27
|
+
[--idle-key-frames on|off] [--log-level debug|info|warning|error]
|
|
19
28
|
"""
|
|
20
29
|
|
|
21
30
|
public static func parse(_ arguments: [String]) throws -> Command {
|
|
@@ -39,6 +48,16 @@ public enum Command: Equatable, Sendable {
|
|
|
39
48
|
let device = try DeviceOptions.take(&options)
|
|
40
49
|
try refuseLeftovers(options)
|
|
41
50
|
return .selfCheck(device)
|
|
51
|
+
case "render":
|
|
52
|
+
guard let job = options.removeValue(forKey: "job"), job.hasPrefix("/") else {
|
|
53
|
+
throw HelperFailure("render needs --job and the absolute path of its job\n\(usage)", status: 400)
|
|
54
|
+
}
|
|
55
|
+
try refuseLeftovers(options)
|
|
56
|
+
return .render(job: job)
|
|
57
|
+
case "capture":
|
|
58
|
+
let capture = try CaptureOptions.take(&options)
|
|
59
|
+
try refuseLeftovers(options)
|
|
60
|
+
return .capture(capture)
|
|
42
61
|
default:
|
|
43
62
|
throw HelperFailure("not a command: \(verb)\n\(usage)", status: 400)
|
|
44
63
|
}
|
|
@@ -122,18 +141,23 @@ public struct ServeOptions: Equatable, Sendable {
|
|
|
122
141
|
|
|
123
142
|
/// What `sim-mirror-helper version` prints.
|
|
124
143
|
public struct VersionReport: Equatable, Sendable, Encodable {
|
|
144
|
+
/// What this helper can do besides serving a simulator, so SimMirror asks only a helper that can.
|
|
145
|
+
public static let features = ["render", "capture"]
|
|
146
|
+
|
|
125
147
|
public let version: String
|
|
126
148
|
public let wire: Int
|
|
127
149
|
public let coreSimulator: String?
|
|
150
|
+
public let features: [String]
|
|
128
151
|
|
|
129
152
|
public init(coreSimulator: String?) {
|
|
130
153
|
version = HelperVersion.current
|
|
131
154
|
wire = Wire.version
|
|
132
155
|
self.coreSimulator = coreSimulator
|
|
156
|
+
features = VersionReport.features
|
|
133
157
|
}
|
|
134
158
|
|
|
135
159
|
enum CodingKeys: String, CodingKey {
|
|
136
|
-
case version, wire, coreSimulator = "core_simulator"
|
|
160
|
+
case version, wire, coreSimulator = "core_simulator", features
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
public func encode(to encoder: Encoder) throws {
|
|
@@ -141,6 +165,7 @@ public struct VersionReport: Equatable, Sendable, Encodable {
|
|
|
141
165
|
try container.encode(version, forKey: .version)
|
|
142
166
|
try container.encode(wire, forKey: .wire)
|
|
143
167
|
try container.encode(coreSimulator, forKey: .coreSimulator)
|
|
168
|
+
try container.encode(features, forKey: .features)
|
|
144
169
|
}
|
|
145
170
|
}
|
|
146
171
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// What `sim-mirror-helper capture` is asked to serve: a cabled real device's screen, read the way QuickTime reads it.
|
|
5
|
+
public struct CaptureOptions: Equatable, Sendable {
|
|
6
|
+
/// The device's hardware UDID, as SimMirror knows it.
|
|
7
|
+
public let udid: String
|
|
8
|
+
/// The device's name, which is what its screen is called among the Mac's capture devices.
|
|
9
|
+
public let name: String
|
|
10
|
+
public let socket: String
|
|
11
|
+
/// The screen as devicectl measures it: its pixels and scale. The capture's own pixels may differ by one.
|
|
12
|
+
public let screen: ScreenGeometry
|
|
13
|
+
/// The capture device SimMirror found this device to be before, tried first.
|
|
14
|
+
public let captureId: String?
|
|
15
|
+
/// A picture of the screen taken another way, to tell apart devices that share a name.
|
|
16
|
+
public let reference: String?
|
|
17
|
+
public let parentPid: Int32?
|
|
18
|
+
public let idleKeyFrames: Bool
|
|
19
|
+
/// How long a picture of the screen is waited for: the first capture after the cable is plugged in switches it
|
|
20
|
+
/// into another mode, measured at 6.4 seconds on an iPhone 14 Pro (2026-09-29).
|
|
21
|
+
public let waitS: Double
|
|
22
|
+
/// How long the capture keeps running after the last picture was asked for, so the next one is quick.
|
|
23
|
+
public let lingerS: Double
|
|
24
|
+
public let logLevel: Log.Level
|
|
25
|
+
|
|
26
|
+
public init(
|
|
27
|
+
udid: String, name: String, socket: String, screen: ScreenGeometry, captureId: String? = nil, reference: String? = nil,
|
|
28
|
+
parentPid: Int32? = nil, idleKeyFrames: Bool = true, waitS: Double = CaptureOptions.defaultWaitS,
|
|
29
|
+
lingerS: Double = CaptureOptions.defaultLingerS, logLevel: Log.Level = .info
|
|
30
|
+
) {
|
|
31
|
+
self.udid = udid
|
|
32
|
+
self.name = name
|
|
33
|
+
self.socket = socket
|
|
34
|
+
self.screen = screen
|
|
35
|
+
self.captureId = captureId
|
|
36
|
+
self.reference = reference
|
|
37
|
+
self.parentPid = parentPid
|
|
38
|
+
self.idleKeyFrames = idleKeyFrames
|
|
39
|
+
self.waitS = waitS
|
|
40
|
+
self.lingerS = lingerS
|
|
41
|
+
self.logLevel = logLevel
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static let defaultWaitS = 15.0
|
|
45
|
+
public static let waitRange = 1.0...120.0
|
|
46
|
+
public static let defaultLingerS = 30.0
|
|
47
|
+
public static let lingerRange = 0.0...600.0
|
|
48
|
+
|
|
49
|
+
static func take(_ options: inout [String: String]) throws -> CaptureOptions {
|
|
50
|
+
func required(_ key: String) throws -> String {
|
|
51
|
+
guard let value = options.removeValue(forKey: key), !value.isEmpty else {
|
|
52
|
+
throw HelperFailure("capture needs --\(key)\n\(Command.usage)", status: 400)
|
|
53
|
+
}
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
func number(_ key: String) throws -> Double {
|
|
57
|
+
let text = try required(key)
|
|
58
|
+
guard let value = Double(text), value.isFinite else { throw HelperFailure("--\(key) is a number, not \(text)", status: 400) }
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
func seconds(_ key: String, _ range: ClosedRange<Double>, otherwise: Double) throws -> Double {
|
|
62
|
+
guard let text = options.removeValue(forKey: key) else { return otherwise }
|
|
63
|
+
guard let value = Double(text), range.contains(value) else {
|
|
64
|
+
throw HelperFailure("--\(key) is \(Int(range.lowerBound)) to \(Int(range.upperBound)) seconds, not \(text)", status: 400)
|
|
65
|
+
}
|
|
66
|
+
return value
|
|
67
|
+
}
|
|
68
|
+
let udid = try required("udid")
|
|
69
|
+
let name = try required("name")
|
|
70
|
+
let socket = try required("socket")
|
|
71
|
+
let screen = try ScreenGeometry.of(pixelWidth: number("width-px"), pixelHeight: number("height-px"), scale: number("scale"))
|
|
72
|
+
let reference = options.removeValue(forKey: "reference")
|
|
73
|
+
if let reference, !reference.hasPrefix("/") {
|
|
74
|
+
throw HelperFailure("--reference is the absolute path of a picture, not \(reference)", status: 400)
|
|
75
|
+
}
|
|
76
|
+
return CaptureOptions(
|
|
77
|
+
udid: udid, name: name, socket: socket, screen: screen, captureId: options.removeValue(forKey: "capture-id"),
|
|
78
|
+
reference: reference, parentPid: try options.removeValue(forKey: "parent-pid").map(Command.pid),
|
|
79
|
+
idleKeyFrames: try options.removeValue(forKey: "idle-key-frames").map(Command.onOff) ?? true,
|
|
80
|
+
waitS: try seconds("wait", waitRange, otherwise: defaultWaitS),
|
|
81
|
+
lingerS: try seconds("linger", lingerRange, otherwise: defaultLingerS),
|
|
82
|
+
logLevel: try options.removeValue(forKey: "log-level").map(Command.logLevel) ?? .info
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/// One of the Mac's capture devices that shows a cabled device's screen.
|
|
89
|
+
public struct CaptureCandidate: Equatable, Sendable {
|
|
90
|
+
public let id: String
|
|
91
|
+
public let name: String
|
|
92
|
+
|
|
93
|
+
public init(id: String, name: String) {
|
|
94
|
+
self.id = id
|
|
95
|
+
self.name = name
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Which capture device is the one asked for.
|
|
100
|
+
///
|
|
101
|
+
/// A capture device is named for the device it shows, but its id is its own -- not the device's UDID -- so it is found
|
|
102
|
+
/// by the id it had before, else by its name, else, when several devices share the name, by which of them shows the
|
|
103
|
+
/// picture of the screen taken another way (`Likeness`).
|
|
104
|
+
public enum CaptureChoice: Equatable, Sendable {
|
|
105
|
+
case use(String)
|
|
106
|
+
/// Several are called that: the one whose screen looks like the reference.
|
|
107
|
+
case compare([String])
|
|
108
|
+
case none
|
|
109
|
+
|
|
110
|
+
public static func pick(_ candidates: [CaptureCandidate], name: String, captureId: String?) -> CaptureChoice {
|
|
111
|
+
if let captureId, candidates.contains(where: { $0.id == captureId }) { return .use(captureId) }
|
|
112
|
+
let named = candidates.filter { $0.name == name }.map(\.id)
|
|
113
|
+
switch named.count {
|
|
114
|
+
case 0: return .none
|
|
115
|
+
case 1: return .use(named[0])
|
|
116
|
+
default: return .compare(named)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// How alike two pictures of a screen are, each shrunk to the same few grey pixels: the mean difference, 0 to 255.
|
|
122
|
+
///
|
|
123
|
+
/// A screen captured by cable and one taken by devicectl a moment apart differ in the clock and little else, so the
|
|
124
|
+
/// device whose picture is nearest the reference is the one the reference was taken of.
|
|
125
|
+
public enum Likeness {
|
|
126
|
+
/// The size pictures are shrunk to before they are compared.
|
|
127
|
+
public static let width = 12
|
|
128
|
+
public static let height = 26
|
|
129
|
+
|
|
130
|
+
public static func distance(_ one: [UInt8], _ other: [UInt8]) -> Double? {
|
|
131
|
+
guard one.count == other.count, !one.isEmpty else { return nil }
|
|
132
|
+
let total = zip(one, other).reduce(0) { $0 + abs(Int($1.0) - Int($1.1)) }
|
|
133
|
+
return Double(total) / Double(one.count)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// The index of the picture nearest the reference, or nil when none can be compared with it.
|
|
137
|
+
public static func nearest(_ reference: [UInt8], among pictures: [[UInt8]?]) -> Int? {
|
|
138
|
+
let scored = pictures.enumerated().compactMap { index, picture in
|
|
139
|
+
picture.flatMap { distance(reference, $0) }.map { (index, $0) }
|
|
140
|
+
}
|
|
141
|
+
return scored.min { $0.1 < $1.1 }?.0
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Whether a captured picture is turned from the portrait SimMirror serves: a device held on its side sends its
|
|
146
|
+
/// screen wider than tall, and it is turned back a quarter so points map onto it as they do in portrait.
|
|
147
|
+
public enum CaptureTurn: Equatable, Sendable {
|
|
148
|
+
case none
|
|
149
|
+
case quarter
|
|
150
|
+
|
|
151
|
+
public static func of(width: Int, height: Int) -> CaptureTurn {
|
|
152
|
+
width > height ? .quarter : .none
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// When the capture runs: while anything holds it -- a stream -- and for a while after the last picture was asked for,
|
|
157
|
+
/// so a device's status bar reads 9:41 and its cable carries a picture only while someone is looking.
|
|
158
|
+
public struct CaptureDemand: Equatable, Sendable {
|
|
159
|
+
public let lingerS: Double
|
|
160
|
+
public private(set) var holders = 0
|
|
161
|
+
public private(set) var lastWanted: Double?
|
|
162
|
+
|
|
163
|
+
public init(lingerS: Double) {
|
|
164
|
+
self.lingerS = lingerS
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Something needs the capture from now until it lets go.
|
|
168
|
+
public mutating func hold(at time: Double) {
|
|
169
|
+
holders += 1
|
|
170
|
+
lastWanted = time
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public mutating func release(at time: Double) {
|
|
174
|
+
holders = max(0, holders - 1)
|
|
175
|
+
lastWanted = time
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Whether the capture should be running at `time`.
|
|
179
|
+
public func running(at time: Double) -> Bool {
|
|
180
|
+
guard let lastWanted else { return false }
|
|
181
|
+
return holders > 0 || time - lastWanted < lingerS
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// When nothing will need the capture any more if nothing asks again, or nil while something holds it.
|
|
185
|
+
public func idleAt() -> Double? {
|
|
186
|
+
guard holders == 0, let lastWanted else { return nil }
|
|
187
|
+
return lastWanted + lingerS
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Whether this process is the one that asks macOS for the Camera, or has to start one that is.
|
|
192
|
+
///
|
|
193
|
+
/// macOS asks permission of a process's responsible process: under a terminal or an agent, that is the terminal or the
|
|
194
|
+
/// agent, which has not asked for the Camera and never will, so the capture quietly gets no pictures. Started again
|
|
195
|
+
/// with its responsibility disclaimed, the helper is its own responsible process and asks for itself.
|
|
196
|
+
public enum Disclaim {
|
|
197
|
+
public static let variable = "SIM_MIRROR_HELPER_DISCLAIMED"
|
|
198
|
+
|
|
199
|
+
public static func needed(_ environment: [String: String]) -> Bool {
|
|
200
|
+
environment[variable] != "1"
|
|
201
|
+
}
|
|
202
|
+
}
|