zeno-mobile-runner 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +497 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +111 -0
  4. package/LICENSE +21 -0
  5. package/README.md +176 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +149 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +154 -0
  43. package/docs/app-integration.md +330 -0
  44. package/docs/benchmarking.md +273 -0
  45. package/docs/client-installation.md +133 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/frameworks.md +72 -0
  50. package/docs/install.md +95 -0
  51. package/docs/npm.md +356 -0
  52. package/docs/protocol-fixtures/README.md +8 -0
  53. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  54. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  55. package/docs/protocol-versioning.md +65 -0
  56. package/docs/protocol.md +560 -0
  57. package/docs/scenario-authoring.md +88 -0
  58. package/docs/trace-privacy.md +88 -0
  59. package/docs/troubleshooting.md +256 -0
  60. package/examples/android-app-auth-probe.json +89 -0
  61. package/examples/android-app-error-state.json +13 -0
  62. package/examples/android-app-login-smoke.json +192 -0
  63. package/examples/android-app-onboarding.json +12 -0
  64. package/examples/android-app-referral-deep-link.json +12 -0
  65. package/examples/android-shim-smoke.json +19 -0
  66. package/examples/demo-failure.json +12 -0
  67. package/examples/demo-fake.json +14 -0
  68. package/examples/ios-dev-client-open-link.json +26 -0
  69. package/examples/ios-dev-client-route-snapshot.json +24 -0
  70. package/examples/ios-shim-smoke.json +23 -0
  71. package/examples/ios-smoke.json +9 -0
  72. package/go.work +3 -0
  73. package/npm/agents.mjs +183 -0
  74. package/npm/app-config.mjs +95 -0
  75. package/npm/build-zmr.mjs +21 -0
  76. package/npm/commands.mjs +104 -0
  77. package/npm/generated-files.mjs +50 -0
  78. package/npm/index.mjs +75 -0
  79. package/npm/init-app.mjs +80 -0
  80. package/npm/package-scripts.mjs +72 -0
  81. package/npm/postinstall.mjs +21 -0
  82. package/npm/scaffold.mjs +179 -0
  83. package/npm/scenarios.mjs +93 -0
  84. package/npm/setup.mjs +69 -0
  85. package/npm/wizard.mjs +117 -0
  86. package/npm/zmr.mjs +23 -0
  87. package/package.json +118 -0
  88. package/schemas/README.md +26 -0
  89. package/schemas/action-result.schema.json +27 -0
  90. package/schemas/capabilities-output.schema.json +98 -0
  91. package/schemas/devices-output.schema.json +25 -0
  92. package/schemas/doctor-output.schema.json +51 -0
  93. package/schemas/explain-output.schema.json +51 -0
  94. package/schemas/import-output.schema.json +23 -0
  95. package/schemas/init-output.schema.json +71 -0
  96. package/schemas/json-rpc.schema.json +55 -0
  97. package/schemas/release-manifest.schema.json +43 -0
  98. package/schemas/release-readiness-output.schema.json +127 -0
  99. package/schemas/run-output.schema.json +43 -0
  100. package/schemas/scenario.schema.json +128 -0
  101. package/schemas/schemas-output.schema.json +26 -0
  102. package/schemas/semantic-snapshot.schema.json +116 -0
  103. package/schemas/snapshot.schema.json +60 -0
  104. package/schemas/trace-event.schema.json +14 -0
  105. package/schemas/trace-manifest.schema.json +59 -0
  106. package/schemas/validate-output.schema.json +42 -0
  107. package/schemas/version-output.schema.json +23 -0
  108. package/schemas/zmr-config.schema.json +75 -0
  109. package/scripts/android-emulator.sh +126 -0
  110. package/scripts/assert-ios-physical-ready.sh +213 -0
  111. package/scripts/benchmark-command.sh +307 -0
  112. package/scripts/benchmark.sh +359 -0
  113. package/scripts/benchmark_gate.py +117 -0
  114. package/scripts/benchmark_result_row.py +88 -0
  115. package/scripts/compare-benchmarks.py +288 -0
  116. package/scripts/create-android-demo-app.sh +342 -0
  117. package/scripts/create-ios-demo-app.sh +261 -0
  118. package/scripts/demo-android-real.sh +232 -0
  119. package/scripts/demo-ios-real.sh +270 -0
  120. package/scripts/demo.sh +464 -0
  121. package/scripts/device-matrix.sh +338 -0
  122. package/scripts/ensure-ios-shim-target.rb +237 -0
  123. package/scripts/install-android-shim.sh +281 -0
  124. package/scripts/install-ios-shim.sh +589 -0
  125. package/scripts/pilot-gate.sh +560 -0
  126. package/scripts/release-readiness.py +838 -0
  127. package/scripts/release-readiness.sh +91 -0
  128. package/scripts/run-android-pilot.sh +561 -0
  129. package/scripts/run-ios-pilot.sh +509 -0
  130. package/shims/android/README.md +21 -0
  131. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  132. package/shims/android/protocol.md +18 -0
  133. package/shims/ios/README.md +50 -0
  134. package/shims/ios/ZMRShim.swift +110 -0
  135. package/shims/ios/ZMRShimUITestCase.swift +518 -0
  136. package/shims/ios/protocol.md +74 -0
  137. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  138. package/src/android.zig +344 -0
  139. package/src/android_device_info.zig +99 -0
  140. package/src/android_emulator.zig +154 -0
  141. package/src/android_screen_recording.zig +112 -0
  142. package/src/android_shell.zig +112 -0
  143. package/src/bundle.zig +124 -0
  144. package/src/bundle_redaction.zig +272 -0
  145. package/src/bundle_tar.zig +123 -0
  146. package/src/cli_devices.zig +97 -0
  147. package/src/cli_doctor.zig +114 -0
  148. package/src/cli_import.zig +70 -0
  149. package/src/cli_info.zig +39 -0
  150. package/src/cli_init.zig +72 -0
  151. package/src/cli_output.zig +467 -0
  152. package/src/cli_run.zig +259 -0
  153. package/src/cli_serve.zig +287 -0
  154. package/src/cli_trace.zig +111 -0
  155. package/src/cli_validate.zig +41 -0
  156. package/src/command.zig +211 -0
  157. package/src/config.zig +305 -0
  158. package/src/config_diagnostics.zig +212 -0
  159. package/src/config_paths.zig +49 -0
  160. package/src/device_registry.zig +37 -0
  161. package/src/doctor.zig +412 -0
  162. package/src/doctor_hints.zig +52 -0
  163. package/src/errors.zig +55 -0
  164. package/src/fake_device.zig +163 -0
  165. package/src/health.zig +28 -0
  166. package/src/importer.zig +343 -0
  167. package/src/importer_json.zig +100 -0
  168. package/src/importer_model.zig +103 -0
  169. package/src/ios.zig +399 -0
  170. package/src/ios_devices.zig +219 -0
  171. package/src/ios_lifecycle.zig +72 -0
  172. package/src/ios_shim.zig +242 -0
  173. package/src/ios_snapshot.zig +20 -0
  174. package/src/json_fields.zig +80 -0
  175. package/src/json_rpc.zig +150 -0
  176. package/src/json_rpc_methods.zig +318 -0
  177. package/src/json_rpc_observation.zig +31 -0
  178. package/src/json_rpc_params.zig +52 -0
  179. package/src/json_rpc_protocol.zig +110 -0
  180. package/src/json_rpc_trace.zig +73 -0
  181. package/src/main.zig +131 -0
  182. package/src/mcp.zig +234 -0
  183. package/src/mcp_protocol.zig +64 -0
  184. package/src/mcp_trace.zig +83 -0
  185. package/src/report.zig +346 -0
  186. package/src/report_html.zig +63 -0
  187. package/src/report_values.zig +27 -0
  188. package/src/run_options.zig +152 -0
  189. package/src/runner.zig +280 -0
  190. package/src/runner_actions.zig +109 -0
  191. package/src/runner_config.zig +6 -0
  192. package/src/runner_diagnostics.zig +268 -0
  193. package/src/runner_events.zig +170 -0
  194. package/src/runner_native.zig +88 -0
  195. package/src/runner_waits.zig +300 -0
  196. package/src/scaffold.zig +472 -0
  197. package/src/scenario.zig +346 -0
  198. package/src/scenario_fields.zig +50 -0
  199. package/src/schema_registry.zig +53 -0
  200. package/src/selector.zig +84 -0
  201. package/src/semantic.zig +171 -0
  202. package/src/trace.zig +315 -0
  203. package/src/trace_json.zig +340 -0
  204. package/src/trace_summary.zig +218 -0
  205. package/src/trace_summary_diagnostic.zig +202 -0
  206. package/src/types.zig +120 -0
  207. package/src/uiautomator.zig +164 -0
  208. package/src/validation.zig +187 -0
  209. package/src/version.zig +22 -0
  210. package/viewer/app.js +373 -0
  211. package/viewer/index.html +126 -0
  212. package/viewer/parser.js +233 -0
  213. package/viewer/styles.css +585 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://zmr.dev/schemas/zmr-config.schema.json",
4
+ "title": "ZMR App Config",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion"],
8
+ "properties": {
9
+ "schemaVersion": { "const": 1 },
10
+ "appId": { "type": "string", "minLength": 1 },
11
+ "android": { "$ref": "#/$defs/platformConfig" },
12
+ "ios": { "$ref": "#/$defs/platformConfig" },
13
+ "artifacts": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "screenshots": { "type": "boolean", "default": true },
18
+ "hierarchy": { "type": "boolean", "default": true },
19
+ "logs": { "type": "boolean", "default": true },
20
+ "screenRecording": { "type": "boolean", "default": false }
21
+ }
22
+ },
23
+ "redaction": {
24
+ "type": "object",
25
+ "additionalProperties": false,
26
+ "properties": {
27
+ "denylistText": { "$ref": "#/$defs/stringList" },
28
+ "allowlistText": { "$ref": "#/$defs/stringList" },
29
+ "denylistResourceIds": { "$ref": "#/$defs/stringList" },
30
+ "allowlistResourceIds": { "$ref": "#/$defs/stringList" }
31
+ }
32
+ },
33
+ "tools": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "adbPath": { "type": "string", "minLength": 1 },
38
+ "emulatorPath": { "type": "string", "minLength": 1 },
39
+ "avdmanagerPath": { "type": "string", "minLength": 1 },
40
+ "androidShimPath": { "type": "string", "minLength": 1 },
41
+ "xcrunPath": { "type": "string", "minLength": 1 },
42
+ "iosShimPath": { "type": "string", "minLength": 1 },
43
+ "zigPath": { "type": "string", "minLength": 1 }
44
+ }
45
+ },
46
+ "scripts": {
47
+ "type": "object",
48
+ "additionalProperties": { "type": "string", "minLength": 1 }
49
+ }
50
+ },
51
+ "$defs": {
52
+ "stringList": {
53
+ "type": "array",
54
+ "items": { "type": "string", "minLength": 1 },
55
+ "default": []
56
+ },
57
+ "platformConfig": {
58
+ "type": "object",
59
+ "additionalProperties": false,
60
+ "properties": {
61
+ "enabled": { "type": "boolean" },
62
+ "defaultDevice": { "type": "string", "minLength": 1 },
63
+ "smokeScenario": { "type": "string", "minLength": 1 },
64
+ "traceDir": { "type": "string", "minLength": 1 },
65
+ "avdName": { "type": "string", "minLength": 1 },
66
+ "restoreSnapshot": { "type": "string", "minLength": 1 },
67
+ "createAvdIfMissing": { "type": "boolean", "default": false },
68
+ "avdSystemImage": { "type": "string", "minLength": 1 },
69
+ "avdDeviceProfile": { "type": "string", "minLength": 1 },
70
+ "resetBeforeRun": { "type": "boolean", "default": false },
71
+ "waitReady": { "type": "boolean", "default": false }
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ AVD="${AVD:-}"
5
+ DEVICE="${DEVICE:-emulator-5554}"
6
+ EMULATOR="${EMULATOR:-emulator}"
7
+ ADB="${ADB:-adb}"
8
+ SNAPSHOT="${SNAPSHOT:-zmr-clean}"
9
+ DRY_RUN=0
10
+
11
+ usage() {
12
+ cat <<'USAGE'
13
+ Usage:
14
+ scripts/android-emulator.sh boot --avd <name> [--device emulator-5554] [--dry-run]
15
+ scripts/android-emulator.sh wait-ready [--device emulator-5554] [--dry-run]
16
+ scripts/android-emulator.sh snapshot-save [--device emulator-5554] [--name zmr-clean] [--dry-run]
17
+ scripts/android-emulator.sh snapshot-load --avd <name> [--name zmr-clean] [--dry-run]
18
+ scripts/android-emulator.sh kill [--device emulator-5554] [--dry-run]
19
+
20
+ Environment:
21
+ EMULATOR emulator binary. Defaults to emulator.
22
+ ADB adb binary. Defaults to adb.
23
+ AVD default AVD name.
24
+ DEVICE default adb serial.
25
+ SNAPSHOT default snapshot name.
26
+ USAGE
27
+ }
28
+
29
+ quote_cmd() {
30
+ local quoted=()
31
+ local arg
32
+ for arg in "$@"; do
33
+ quoted+=("$(printf '%q' "$arg")")
34
+ done
35
+ printf '%s\n' "${quoted[*]}"
36
+ }
37
+
38
+ run() {
39
+ echo "+ $(quote_cmd "$@")"
40
+ if [[ "$DRY_RUN" -eq 0 ]]; then
41
+ "$@"
42
+ fi
43
+ }
44
+
45
+ die() {
46
+ echo "error: $*" >&2
47
+ exit 2
48
+ }
49
+
50
+ require_value() {
51
+ local flag="$1"
52
+ local value="${2-}"
53
+ if [[ -z "$value" || "$value" == --* ]]; then
54
+ die "$flag requires a value"
55
+ fi
56
+ printf '%s\n' "$value"
57
+ }
58
+
59
+ [[ $# -gt 0 ]] || {
60
+ usage >&2
61
+ exit 2
62
+ }
63
+
64
+ COMMAND="$1"
65
+ shift
66
+
67
+ while [[ $# -gt 0 ]]; do
68
+ case "$1" in
69
+ --avd)
70
+ AVD="$(require_value "$1" "${2-}")"
71
+ shift 2
72
+ ;;
73
+ --device)
74
+ DEVICE="$(require_value "$1" "${2-}")"
75
+ shift 2
76
+ ;;
77
+ --name)
78
+ SNAPSHOT="$(require_value "$1" "${2-}")"
79
+ shift 2
80
+ ;;
81
+ --dry-run)
82
+ DRY_RUN=1
83
+ shift
84
+ ;;
85
+ -h|--help)
86
+ usage
87
+ exit 0
88
+ ;;
89
+ *)
90
+ die "unknown argument: $1"
91
+ ;;
92
+ esac
93
+ done
94
+
95
+ case "$COMMAND" in
96
+ boot)
97
+ [[ -n "$AVD" ]] || die "--avd or AVD is required"
98
+ run "$EMULATOR" -avd "$AVD" -no-snapshot-load -netdelay none -netspeed full
99
+ ;;
100
+ wait-ready)
101
+ run "$ADB" -s "$DEVICE" wait-for-device
102
+ if [[ "$DRY_RUN" -eq 1 ]]; then
103
+ echo "+ wait until sys.boot_completed is 1"
104
+ exit 0
105
+ fi
106
+ for _ in $(seq 1 120); do
107
+ value="$("$ADB" -s "$DEVICE" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
108
+ [[ "$value" == "1" ]] && exit 0
109
+ sleep 2
110
+ done
111
+ die "emulator did not finish booting"
112
+ ;;
113
+ snapshot-save)
114
+ run "$ADB" -s "$DEVICE" emu avd snapshot save "$SNAPSHOT"
115
+ ;;
116
+ snapshot-load)
117
+ [[ -n "$AVD" ]] || die "--avd or AVD is required"
118
+ run "$EMULATOR" -avd "$AVD" -snapshot "$SNAPSHOT" -netdelay none -netspeed full
119
+ ;;
120
+ kill)
121
+ run "$ADB" -s "$DEVICE" emu kill
122
+ ;;
123
+ *)
124
+ die "unknown command: $COMMAND"
125
+ ;;
126
+ esac
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [[ -h "$SOURCE" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ if [[ "$SOURCE" != /* ]]; then
9
+ SOURCE="$SOURCE_DIR/$SOURCE"
10
+ fi
11
+ done
12
+
13
+ ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
14
+ cd "$ROOT"
15
+
16
+ if [[ -n "${ZMR_BIN:-}" ]]; then
17
+ ZMR="$ZMR_BIN"
18
+ elif [[ -x "$ROOT/zig-out/bin/zmr" ]]; then
19
+ ZMR="$ROOT/zig-out/bin/zmr"
20
+ else
21
+ ZMR="zmr"
22
+ fi
23
+ DEVICE=""
24
+ XCRUN=""
25
+ XCRUN_PROVIDED=0
26
+ EVIDENCE_OUT=""
27
+ ATTEMPTS="${ZMR_IOS_READY_ATTEMPTS:-3}"
28
+ RETRY_DELAY_SECONDS="${ZMR_IOS_READY_RETRY_DELAY_SECONDS:-1}"
29
+ START_MS="$(python3 - <<'PY'
30
+ import time
31
+ print(int(time.time() * 1000))
32
+ PY
33
+ )"
34
+
35
+ usage() {
36
+ cat <<'USAGE'
37
+ Usage:
38
+ scripts/assert-ios-physical-ready.sh [--zmr <path>] [--xcrun <path>] [--device <identifier>] [--evidence-out <path>]
39
+
40
+ Fails unless zmr reports at least one ready physical iOS device. When --device
41
+ is supplied, that exact CoreDevice identifier from `zmr devices --json
42
+ --platform ios --ios-device-type physical` must be present and ready.
43
+
44
+ When --evidence-out is supplied, a successful check appends a JSONL row that
45
+ can be consumed by zmr-release-readiness.
46
+ USAGE
47
+ }
48
+
49
+ die() {
50
+ echo "error: $*" >&2
51
+ exit 2
52
+ }
53
+
54
+ require_value() {
55
+ local flag="$1"
56
+ local value="${2-}"
57
+ if [[ -z "$value" || "$value" == --* ]]; then
58
+ die "$flag requires a value"
59
+ fi
60
+ printf '%s\n' "$value"
61
+ }
62
+
63
+ append_evidence() {
64
+ [[ -n "$EVIDENCE_OUT" ]] || return 0
65
+
66
+ local end_ms duration_ms command
67
+ end_ms="$(python3 - <<'PY'
68
+ import time
69
+ print(int(time.time() * 1000))
70
+ PY
71
+ )"
72
+ duration_ms="$((end_ms - START_MS))"
73
+ if [[ -n "$DEVICE" ]]; then
74
+ command="scripts/assert-ios-physical-ready.sh --device $DEVICE"
75
+ else
76
+ command="scripts/assert-ios-physical-ready.sh"
77
+ fi
78
+ if [[ -n "$XCRUN" ]]; then
79
+ command="$command --xcrun $XCRUN"
80
+ fi
81
+
82
+ mkdir -p "$(dirname "$EVIDENCE_OUT")"
83
+ python3 - "$EVIDENCE_OUT" "physical iOS readiness" "ios-physical-ready" "passed" "$duration_ms" "$command" "$DEVICE" <<'PY'
84
+ import json
85
+ import sys
86
+
87
+ path, name, mode, status, duration_ms, command, device_id = sys.argv[1:]
88
+ row = {
89
+ "name": name,
90
+ "mode": mode,
91
+ "status": status,
92
+ "durationMs": int(duration_ms),
93
+ "command": command,
94
+ }
95
+ if device_id:
96
+ row["deviceId"] = device_id
97
+ with open(path, "a", encoding="utf-8") as fh:
98
+ fh.write(json.dumps(row, separators=(",", ":")) + "\n")
99
+ PY
100
+ }
101
+
102
+ while [[ $# -gt 0 ]]; do
103
+ case "$1" in
104
+ --zmr)
105
+ ZMR="$(require_value "$1" "${2-}")"
106
+ shift 2
107
+ ;;
108
+ --xcrun)
109
+ XCRUN="$(require_value "$1" "${2-}")"
110
+ XCRUN_PROVIDED=1
111
+ shift 2
112
+ ;;
113
+ --device)
114
+ DEVICE="$(require_value "$1" "${2-}")"
115
+ shift 2
116
+ ;;
117
+ --evidence-out)
118
+ EVIDENCE_OUT="$(require_value "$1" "${2-}")"
119
+ shift 2
120
+ ;;
121
+ -h|--help)
122
+ usage
123
+ exit 0
124
+ ;;
125
+ *)
126
+ die "unknown argument: $1"
127
+ ;;
128
+ esac
129
+ done
130
+
131
+ [[ -n "$ZMR" ]] || die "--zmr must be non-empty"
132
+ if [[ "$XCRUN_PROVIDED" -eq 1 && -z "$XCRUN" ]]; then
133
+ die "--xcrun must be non-empty"
134
+ fi
135
+ [[ "$ATTEMPTS" =~ ^[0-9]+$ && "$ATTEMPTS" -ge 1 ]] || die "ZMR_IOS_READY_ATTEMPTS must be a positive integer"
136
+ [[ "$RETRY_DELAY_SECONDS" =~ ^[0-9]+$ ]] || die "ZMR_IOS_READY_RETRY_DELAY_SECONDS must be a non-negative integer"
137
+
138
+ devices_json=""
139
+ last_error=""
140
+ attempt=1
141
+ zmr_devices_args=(devices --json --platform ios --ios-device-type physical)
142
+ if [[ -n "$XCRUN" ]]; then
143
+ zmr_devices_args+=(--xcrun "$XCRUN")
144
+ fi
145
+ while [[ "$attempt" -le "$ATTEMPTS" ]]; do
146
+ error_file="$(mktemp)"
147
+ if devices_json="$("$ZMR" "${zmr_devices_args[@]}" 2>"$error_file")"; then
148
+ rm -f "$error_file"
149
+ break
150
+ fi
151
+ last_error="$(cat "$error_file")"
152
+ rm -f "$error_file"
153
+ if [[ "$attempt" -lt "$ATTEMPTS" ]]; then
154
+ sleep "$RETRY_DELAY_SECONDS"
155
+ fi
156
+ attempt="$((attempt + 1))"
157
+ done
158
+
159
+ if [[ -z "$devices_json" ]]; then
160
+ if [[ -n "$last_error" ]]; then
161
+ printf '%s\n' "$last_error" >&2
162
+ fi
163
+ echo "error[setup.ios.physical_devices_unavailable]: unable to list physical iOS devices after $ATTEMPTS attempt(s)" >&2
164
+ exit 3
165
+ fi
166
+
167
+ if ZMR_DEVICES_JSON="$devices_json" python3 - "$DEVICE" <<'PY'
168
+ import json
169
+ import os
170
+ import sys
171
+
172
+ target = sys.argv[1] or None
173
+
174
+ try:
175
+ data = json.loads(os.environ["ZMR_DEVICES_JSON"])
176
+ except Exception as exc:
177
+ print(f"error[setup.ios.devices_json_invalid]: failed to parse zmr devices JSON: {exc}", file=sys.stderr)
178
+ sys.exit(3)
179
+
180
+ devices = data.get("devices")
181
+ if not isinstance(devices, list):
182
+ print("error[setup.ios.devices_json_invalid]: zmr devices JSON is missing devices[]", file=sys.stderr)
183
+ sys.exit(3)
184
+
185
+ if target:
186
+ for device in devices:
187
+ if device.get("serial") == target:
188
+ if device.get("ready") is True:
189
+ print(f"physical iOS device ready: {target}")
190
+ sys.exit(0)
191
+ state = device.get("state") or "unknown"
192
+ print(
193
+ f"error[setup.ios.physical_device_not_ready]: physical iOS device is not ready: {target} (state: {state})",
194
+ file=sys.stderr,
195
+ )
196
+ sys.exit(3)
197
+ print(f"error[setup.ios.physical_device_not_found]: physical iOS device was not found: {target}", file=sys.stderr)
198
+ sys.exit(3)
199
+
200
+ for device in devices:
201
+ if device.get("ready") is True:
202
+ print(f"physical iOS device ready: {device.get('serial', '<unknown>')}")
203
+ sys.exit(0)
204
+
205
+ states = ", ".join(f"{d.get('serial', '<unknown>')}:{d.get('state', 'unknown')}" for d in devices) or "none"
206
+ print(f"error[setup.ios.no_ready_physical_devices]: no ready physical iOS devices found ({states})", file=sys.stderr)
207
+ sys.exit(3)
208
+ PY
209
+ then
210
+ append_evidence
211
+ else
212
+ exit $?
213
+ fi