zig-mobile-runner 0.1.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.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -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 +144 -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 +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -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/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,86 @@
1
+ import Foundation
2
+ import ZMRClient
3
+
4
+ struct Options {
5
+ var zmr = "zig-out/bin/zmr"
6
+ var adb = "tests/fake-adb.sh"
7
+ var device = "fake-android-1"
8
+ var appID = "com.example.mobiletest"
9
+ var traceDir = "traces/demo-swift-client"
10
+ var traceOut = "traces/demo-swift-client-redacted.zmrtrace"
11
+ }
12
+
13
+ func parseOptions(_ arguments: [String]) throws -> Options {
14
+ var options = Options()
15
+ var index = 0
16
+ while index < arguments.count {
17
+ let argument = arguments[index]
18
+ guard index + 1 < arguments.count else {
19
+ throw NSError(domain: "ZMRFakeSession", code: 2, userInfo: [NSLocalizedDescriptionKey: "\(argument) requires a value"])
20
+ }
21
+ let value = arguments[index + 1]
22
+ switch argument {
23
+ case "--zmr": options.zmr = value
24
+ case "--adb": options.adb = value
25
+ case "--device": options.device = value
26
+ case "--app-id": options.appID = value
27
+ case "--trace-dir": options.traceDir = value
28
+ case "--trace-out": options.traceOut = value
29
+ default:
30
+ throw NSError(domain: "ZMRFakeSession", code: 2, userInfo: [NSLocalizedDescriptionKey: "unknown argument: \(argument)"])
31
+ }
32
+ index += 2
33
+ }
34
+ return options
35
+ }
36
+
37
+ func object(_ value: Any) throws -> [String: Any] {
38
+ guard let object = value as? [String: Any] else {
39
+ throw ZMRError.invalidResponse
40
+ }
41
+ return object
42
+ }
43
+
44
+ do {
45
+ let options = try parseOptions(Array(CommandLine.arguments.dropFirst()))
46
+ let client = ZMRClient(
47
+ executable: options.zmr,
48
+ arguments: [
49
+ "serve",
50
+ "--transport", "stdio",
51
+ "--device", options.device,
52
+ "--app-id", options.appID,
53
+ "--adb", options.adb,
54
+ "--trace-dir", options.traceDir
55
+ ]
56
+ )
57
+ try client.start()
58
+ defer { client.close() }
59
+
60
+ let capabilities = try object(try client.call("runner.capabilities"))
61
+ try client.createSession()
62
+ _ = try client.call("app.openLink", params: ["url": "exampleapp://swift-client"])
63
+ _ = try client.call("wait.until", params: ["visible": ["text": "Dashboard"], "timeoutMs": 1000])
64
+ _ = try client.call("ui.tap", params: ["selector": ["text": "Sign in"]])
65
+ _ = try client.call("ui.type", params: ["text": "agent@example.com", "selector": ["resourceId": "email-login-email-input"]])
66
+ _ = try client.call("assert.notVisible", params: ["selector": ["text": "Application has crashed"], "timeoutMs": 100])
67
+ _ = try client.assertHealthy(timeoutMs: 100)
68
+ let snapshot = try client.snapshot()
69
+ let exported = try object(try client.call("trace.export", params: ["out": options.traceOut, "redact": true, "includeScreenshots": true]))
70
+ let events = try object(try client.call("trace.events", params: ["afterSeq": 0, "limit": 10]))
71
+
72
+ let nodes = snapshot["nodes"] as? [Any] ?? []
73
+ let summary: [String: Any] = [
74
+ "protocolVersion": capabilities["protocolVersion"] ?? "",
75
+ "activePackage": snapshot["activePackage"] ?? "",
76
+ "nodes": nodes.count,
77
+ "events": events["nextSeq"] ?? 0,
78
+ "traceDir": options.traceDir,
79
+ "traceOut": exported["out"] ?? options.traceOut
80
+ ]
81
+ let data = try JSONSerialization.data(withJSONObject: summary, options: [.sortedKeys])
82
+ print(String(data: data, encoding: .utf8) ?? "{}")
83
+ } catch {
84
+ fputs("\(error)\n", stderr)
85
+ exit(1)
86
+ }
@@ -0,0 +1,34 @@
1
+ # ZMR TypeScript Reference Client
2
+
3
+ Zero-dependency ESM client for ZMR's newline-delimited JSON-RPC protocol.
4
+
5
+ ```js
6
+ import { createZmrClient } from "./index.mjs";
7
+
8
+ const zmr = createZmrClient({
9
+ command: "zmr",
10
+ args: [
11
+ "serve",
12
+ "--transport", "stdio",
13
+ "--device", "emulator-5554",
14
+ "--app-id", "com.example.mobiletest",
15
+ "--trace-dir", "traces/agent-session",
16
+ ],
17
+ });
18
+
19
+ try {
20
+ await zmr.createSession();
21
+ await zmr.openLink("exampleapp://e2e-auth?probe=1");
22
+ await zmr.waitUntil({ text: "E2E auth probe" }, { timeoutMs: 30000 });
23
+ const snapshot = await zmr.snapshot();
24
+ const events = await zmr.traceEvents(0, { limit: 100 });
25
+ console.log(snapshot.nodes);
26
+ console.log(events.events.length);
27
+ await zmr.exportTrace("traces/agent-session-redacted.zmrtrace", { redact: true, omitScreenshots: true });
28
+ } finally {
29
+ await zmr.close();
30
+ }
31
+ ```
32
+
33
+ The runtime is plain JavaScript (`index.mjs`) with TypeScript declarations
34
+ (`index.d.ts`) so consumers can use it without a build step.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import url from "node:url";
4
+ import { createZmrClient } from "../index.mjs";
5
+
6
+ const root = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "../../..");
7
+
8
+ const zmr = createZmrClient({
9
+ command: path.join(root, "zig-out", "bin", "zmr"),
10
+ args: [
11
+ "serve",
12
+ "--transport", "stdio",
13
+ "--device", "fake-android-1",
14
+ "--app-id", "com.example.mobiletest",
15
+ "--adb", path.join(root, "tests", "fake-adb.sh"),
16
+ "--trace-dir", "traces/demo-typescript-client",
17
+ ],
18
+ });
19
+
20
+ try {
21
+ const capabilities = await zmr.capabilities();
22
+ await zmr.createSession();
23
+ await zmr.openLink("exampleapp://e2e-auth?probe=1");
24
+ await zmr.assertHealthy({ timeoutMs: 100 });
25
+ const snapshot = await zmr.snapshot();
26
+ const events = await zmr.traceEvents(0, { limit: 20 });
27
+ await zmr.exportTrace("traces/demo-typescript-client-redacted.zmrtrace", { redact: true, omitScreenshots: true });
28
+ console.log(JSON.stringify({
29
+ protocolVersion: capabilities.protocolVersion,
30
+ activePackage: snapshot.activePackage,
31
+ nodes: snapshot.nodes.length,
32
+ events: events.events.length,
33
+ }));
34
+ } finally {
35
+ await zmr.close();
36
+ }
@@ -0,0 +1,144 @@
1
+ export interface ZmrClientOptions {
2
+ command: string;
3
+ args?: string[];
4
+ cwd?: string;
5
+ env?: Record<string, string | undefined>;
6
+ stderr?: "inherit" | "ignore" | "pipe";
7
+ }
8
+
9
+ export interface Selector {
10
+ id?: string;
11
+ resourceId?: string;
12
+ text?: string;
13
+ textContains?: string;
14
+ contentDesc?: string;
15
+ contentDescContains?: string;
16
+ className?: string;
17
+ }
18
+
19
+ export interface Viewport {
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface UiNode {
25
+ stableId: string;
26
+ className: string;
27
+ resourceId?: string | null;
28
+ text?: string | null;
29
+ contentDesc?: string | null;
30
+ bounds: { x: number; y: number; width: number; height: number };
31
+ enabled: boolean;
32
+ visible: boolean;
33
+ selected: boolean;
34
+ }
35
+
36
+ export interface ObservationSnapshot {
37
+ id: string;
38
+ timestampMs: number;
39
+ viewport: Viewport;
40
+ displayDensityDpi?: number | null;
41
+ activePackage?: string | null;
42
+ activeActivity?: string | null;
43
+ screenshotArtifact?: string | null;
44
+ treeArtifact?: string | null;
45
+ focusedNodeId?: string | null;
46
+ logDelta?: string | null;
47
+ nodes: UiNode[];
48
+ }
49
+
50
+ export interface SemanticNode {
51
+ id: string;
52
+ role: "button" | "textbox" | "switch" | "checkbox" | "radio" | "image" | "text" | "node" | string;
53
+ name: string;
54
+ selector: Record<string, string>;
55
+ source: {
56
+ className: string;
57
+ resourceId?: string | null;
58
+ text?: string | null;
59
+ contentDesc?: string | null;
60
+ };
61
+ bounds: { x: number; y: number; width: number; height: number; centerX: number; centerY: number };
62
+ enabled: boolean;
63
+ visible: boolean;
64
+ selected: boolean;
65
+ interactive: boolean;
66
+ recommendedAction?: "tap" | "type" | null | string;
67
+ }
68
+
69
+ export interface SemanticSnapshot {
70
+ id: string;
71
+ timestampMs: number;
72
+ viewport: Viewport;
73
+ activePackage?: string | null;
74
+ activeActivity?: string | null;
75
+ focusedNodeId?: string | null;
76
+ nodes: SemanticNode[];
77
+ summary: {
78
+ nodeCount: number;
79
+ interactiveCount: number;
80
+ visibleText: string[];
81
+ };
82
+ }
83
+
84
+ export interface PlatformSupport {
85
+ status: "supported" | "preview" | "unsupported" | string;
86
+ deviceTypes: string[];
87
+ automation: string[];
88
+ physicalDevices?: boolean;
89
+ }
90
+
91
+ export interface Capabilities {
92
+ name: string;
93
+ version: string;
94
+ protocolVersion: string;
95
+ platforms: string[];
96
+ platformSupport?: Record<string, PlatformSupport>;
97
+ iosPreview?: boolean;
98
+ transports: string[];
99
+ methods: string[];
100
+ }
101
+
102
+ export interface DeviceInfo {
103
+ serial: string;
104
+ state: string;
105
+ ready: boolean;
106
+ }
107
+
108
+ export interface ZmrClient {
109
+ request<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
110
+ capabilities(): Promise<Capabilities>;
111
+ createSession(): Promise<{ sessionId: string }>;
112
+ closeSession(): Promise<boolean>;
113
+ devices(): Promise<DeviceInfo[]>;
114
+ launch(): Promise<boolean>;
115
+ stop(): Promise<boolean>;
116
+ clearState(): Promise<boolean>;
117
+ openLink(url: string): Promise<boolean>;
118
+ snapshot(): Promise<ObservationSnapshot>;
119
+ semanticSnapshot(): Promise<SemanticSnapshot>;
120
+ tap(selector: Selector): Promise<boolean>;
121
+ typeText(text: string, options?: { selector?: Selector }): Promise<boolean>;
122
+ eraseText(options?: { selector?: Selector; maxChars?: number }): Promise<boolean>;
123
+ hideKeyboard(): Promise<boolean>;
124
+ swipe(input: { x1: number; y1: number; x2: number; y2: number; durationMs?: number }): Promise<boolean>;
125
+ pressBack(): Promise<boolean>;
126
+ scrollUntilVisible(selector: Selector, options?: { direction?: "up" | "down"; timeoutMs?: number }): Promise<boolean>;
127
+ waitUntil(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
128
+ waitAny(selectors: Selector[], options?: { timeoutMs?: number }): Promise<{ matchedIndex: number } | false>;
129
+ waitGone(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
130
+ assertVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
131
+ assertNotVisible(selector: Selector, options?: { timeoutMs?: number }): Promise<boolean>;
132
+ assertHealthy(options?: { timeoutMs?: number }): Promise<boolean>;
133
+ exportTrace(out: string, options?: { redact?: boolean; omitScreenshots?: boolean }): Promise<Record<string, unknown>>;
134
+ traceEvents(afterSeq?: number, options?: { limit?: number }): Promise<Record<string, unknown>>;
135
+ close(): Promise<void>;
136
+ }
137
+
138
+ export class ZmrRpcError extends Error {
139
+ code?: number;
140
+ publicCode?: string;
141
+ data?: unknown;
142
+ }
143
+
144
+ export function createZmrClient(options: ZmrClientOptions): ZmrClient;
@@ -0,0 +1,192 @@
1
+ import { spawn } from "node:child_process";
2
+ import readline from "node:readline";
3
+
4
+ export class ZmrRpcError extends Error {
5
+ constructor(error) {
6
+ super(error?.message || "ZMR JSON-RPC error");
7
+ this.name = "ZmrRpcError";
8
+ this.code = error?.code;
9
+ this.publicCode = error?.publicCode;
10
+ this.data = error?.data;
11
+ }
12
+ }
13
+
14
+ export function createZmrClient(options) {
15
+ return new ZmrClient(options);
16
+ }
17
+
18
+ export class ZmrClient {
19
+ #child;
20
+ #nextId = 1;
21
+ #pending = new Map();
22
+ #closed = false;
23
+
24
+ constructor(options) {
25
+ if (!options?.command) throw new Error("createZmrClient requires command");
26
+ this.#child = spawn(options.command, options.args ?? [], {
27
+ cwd: options.cwd,
28
+ env: options.env,
29
+ stdio: ["pipe", "pipe", options.stderr ?? "inherit"],
30
+ });
31
+
32
+ const lines = readline.createInterface({ input: this.#child.stdout });
33
+ lines.on("line", (line) => this.#handleLine(line));
34
+ this.#child.on("error", (error) => this.#rejectAll(error));
35
+ this.#child.on("exit", (code, signal) => {
36
+ this.#closed = true;
37
+ if (this.#pending.size > 0) {
38
+ this.#rejectAll(new Error(`zmr process exited with ${signal ?? code}`));
39
+ }
40
+ });
41
+ }
42
+
43
+ request(method, params = {}) {
44
+ if (this.#closed) return Promise.reject(new Error("zmr client is closed"));
45
+ const id = this.#nextId++;
46
+ const message = { jsonrpc: "2.0", id, method, params };
47
+ return new Promise((resolve, reject) => {
48
+ this.#pending.set(id, { resolve, reject });
49
+ this.#child.stdin.write(`${JSON.stringify(message)}\n`, (error) => {
50
+ if (!error) return;
51
+ this.#pending.delete(id);
52
+ reject(error);
53
+ });
54
+ });
55
+ }
56
+
57
+ capabilities() {
58
+ return this.request("runner.capabilities", {});
59
+ }
60
+
61
+ createSession() {
62
+ return this.request("session.create", {});
63
+ }
64
+
65
+ closeSession() {
66
+ return this.request("session.close", {});
67
+ }
68
+
69
+ devices() {
70
+ return this.request("device.list", {});
71
+ }
72
+
73
+ launch() {
74
+ return this.request("app.launch", {});
75
+ }
76
+
77
+ stop() {
78
+ return this.request("app.stop", {});
79
+ }
80
+
81
+ clearState() {
82
+ return this.request("app.clearState", {});
83
+ }
84
+
85
+ openLink(url) {
86
+ return this.request("app.openLink", { url });
87
+ }
88
+
89
+ snapshot() {
90
+ return this.request("observe.snapshot", {});
91
+ }
92
+
93
+ semanticSnapshot() {
94
+ return this.request("observe.semanticSnapshot", {});
95
+ }
96
+
97
+ tap(selector) {
98
+ return this.request("ui.tap", { selector });
99
+ }
100
+
101
+ typeText(text, options = {}) {
102
+ return this.request("ui.type", { ...options, text });
103
+ }
104
+
105
+ eraseText(options = {}) {
106
+ return this.request("ui.eraseText", options);
107
+ }
108
+
109
+ hideKeyboard() {
110
+ return this.request("ui.hideKeyboard", {});
111
+ }
112
+
113
+ swipe(input) {
114
+ return this.request("ui.swipe", input);
115
+ }
116
+
117
+ pressBack() {
118
+ return this.request("ui.pressBack", {});
119
+ }
120
+
121
+ scrollUntilVisible(selector, options = {}) {
122
+ return this.request("ui.scrollUntilVisible", { selector, ...options });
123
+ }
124
+
125
+ waitUntil(selector, options = {}) {
126
+ return this.request("wait.until", { visible: selector, ...options });
127
+ }
128
+
129
+ waitAny(selectors, options = {}) {
130
+ return this.request("wait.any", { selectors, ...options });
131
+ }
132
+
133
+ waitGone(selector, options = {}) {
134
+ return this.request("wait.gone", { selector, ...options });
135
+ }
136
+
137
+ assertVisible(selector, options = {}) {
138
+ return this.request("assert.visible", { selector, ...options });
139
+ }
140
+
141
+ assertNotVisible(selector, options = {}) {
142
+ return this.request("assert.notVisible", { selector, ...options });
143
+ }
144
+
145
+ assertHealthy(options = {}) {
146
+ return this.request("assert.healthy", options);
147
+ }
148
+
149
+ exportTrace(out, options = {}) {
150
+ return this.request("trace.export", { out, ...options });
151
+ }
152
+
153
+ traceEvents(afterSeq = 0, options = {}) {
154
+ return this.request("trace.events", { afterSeq, ...options });
155
+ }
156
+
157
+ async close() {
158
+ if (this.#closed) return;
159
+ this.#closed = true;
160
+ this.#child.stdin.end();
161
+ if (this.#child.exitCode == null && this.#child.signalCode == null) {
162
+ this.#child.kill();
163
+ }
164
+ }
165
+
166
+ #handleLine(line) {
167
+ if (!line.trim()) return;
168
+ let message;
169
+ try {
170
+ message = JSON.parse(line);
171
+ } catch (error) {
172
+ this.#rejectAll(error);
173
+ return;
174
+ }
175
+
176
+ const pending = this.#pending.get(message.id);
177
+ if (!pending) return;
178
+ this.#pending.delete(message.id);
179
+ if (message.error) {
180
+ pending.reject(new ZmrRpcError(message.error));
181
+ } else {
182
+ pending.resolve(message.result);
183
+ }
184
+ }
185
+
186
+ #rejectAll(error) {
187
+ for (const pending of this.#pending.values()) {
188
+ pending.reject(error);
189
+ }
190
+ this.#pending.clear();
191
+ }
192
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@zmr/client",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.mjs",
6
+ "types": "index.d.ts",
7
+ "private": true
8
+ }
@@ -0,0 +1,31 @@
1
+ # 0001: Agent-Native Runner Boundary
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ ZMR is intended for AI agents and deterministic automation, but embedding an
10
+ LLM inside the runner would make device control harder to test, version, and
11
+ secure. The runner needs a small, stable control surface that external agents,
12
+ scripts, and SDKs can consume.
13
+
14
+ ## Decision
15
+
16
+ ZMR does not embed an LLM. Zig owns orchestration, device control,
17
+ JSON-RPC/session handling, scenario execution, wait/assertion logic, and trace
18
+ generation. External agents decide what to do next by reading structured
19
+ observations and calling typed actions.
20
+
21
+ The public agent contract is JSON-RPC over stdio or localhost TCP, plus
22
+ machine-readable CLI JSON and public schemas.
23
+
24
+ ## Consequences
25
+
26
+ - The core runner remains deterministic and testable without model calls.
27
+ - Agents can be swapped without changing device adapters.
28
+ - Trace output records what happened at the protocol/action layer, which makes
29
+ failures explainable without replaying model reasoning.
30
+ - Higher-level planning, natural-language test generation, and app-specific
31
+ heuristics belong in external clients or app-local `.zmr/` assets.
@@ -0,0 +1,39 @@
1
+ # 0002: App-Local `.zmr/` Contract
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ Mobile app repositories need a predictable place for runner configuration,
10
+ smoke scenarios, shim commands, and setup scripts. ZMR should be installable as
11
+ an npm dev dependency, but it should also work from source checkouts and
12
+ release archives.
13
+
14
+ ## Decision
15
+
16
+ `.zmr/` is the app-local contract. The default config file is
17
+ `.zmr/config.json`, validated by `schemas/config.schema.json`. ZMR commands
18
+ auto-discover the config from the app checkout, and explicit CLI flags always
19
+ override config defaults.
20
+
21
+ `zmr-wizard` and `zmr init --app` scaffold the same shape:
22
+
23
+ - `.zmr/config.json`
24
+ - `.zmr/android-smoke.json`
25
+ - `.zmr/ios-smoke.json`
26
+ - `.zmr/AGENTS.md`
27
+ - optional shim commands and source files
28
+ - app package script suggestions
29
+ - `traces/` ignored by default
30
+
31
+ ## Consequences
32
+
33
+ - App-specific scenarios and private traces stay in the app repository, not in
34
+ the public ZMR repo.
35
+ - npm, source, and release-archive installs share one integration model.
36
+ - Agents can discover the same setup state humans use through `zmr doctor`,
37
+ `zmr schemas`, `.zmr/config.json`, and `.zmr/AGENTS.md`.
38
+ - Generated files need backwards-compatible schema handling and clear
39
+ diagnostics because app repositories may pin older versions.
@@ -0,0 +1,41 @@
1
+ # 0003: iOS XCTest Shim
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ `xcrun simctl` is good for simulator lifecycle, screenshots, logs, deep links,
10
+ and app install/launch, but it is not enough for robust selector-driven UI
11
+ automation. `xcrun devicectl` provides physical-device lifecycle operations,
12
+ but it still does not provide the selector-grade UI semantics ZMR needs. ZMR
13
+ needs iOS behavior that matches Android scenario semantics where the platforms
14
+ overlap.
15
+
16
+ ## Decision
17
+
18
+ iOS support uses three layers:
19
+
20
+ - `xcrun simctl` for lifecycle, install, launch, stop, open link, clear state,
21
+ screenshots, logs, and device discovery.
22
+ - `xcrun devicectl` for physical-device discovery, install, launch, deep-link
23
+ launch, clear-state uninstall, and best-effort stop.
24
+ - An app-local XCTest/XCUIAutomation shim for hierarchy snapshots, element
25
+ queries, screenshots, tap, type, erase text, keyboard control, swipe, and app
26
+ state on simulators and physical devices.
27
+
28
+ V1 iOS clear-state semantics are best-effort app uninstall by bundle id.
29
+ Physical-device log capture remains limited until a supported capture channel is
30
+ available.
31
+
32
+ ## Consequences
33
+
34
+ - iOS selector actions use platform automation APIs instead of coordinate-only
35
+ shelling.
36
+ - App repositories must wire a UI test target when they need selector-grade iOS
37
+ runs.
38
+ - The internal shim protocol can evolve during the dev preview, while public
39
+ behavior remains the ZMR CLI, scenario format, JSON-RPC methods, and schemas.
40
+ - Physical iOS support depends on local signing, provisioning, Developer Mode,
41
+ pairing, and `devicectl` transport state.
@@ -0,0 +1,37 @@
1
+ # 0004: Benchmark Claims And Baseline Collection
2
+
3
+ ## Status
4
+
5
+ Accepted.
6
+
7
+ ## Context
8
+
9
+ ZMR should eventually demonstrate speed, reliability, and diagnostics against
10
+ existing app-local automation, but public fixtures cannot assume a private app,
11
+ private credentials, or a specific third-party runner.
12
+
13
+ ## Decision
14
+
15
+ Public ZMR benchmarking remains tool-agnostic:
16
+
17
+ - `zmr-benchmark` records repeated ZMR scenario runs and can append rows to a
18
+ shared comparison JSONL file.
19
+ - `zmr-benchmark-command` records repeated app-local baseline commands.
20
+ - `zmr-compare-benchmarks` compares normalized candidate and baseline rows and
21
+ can enforce candidate pass-rate, failure-count, mean-speedup, and p95-speedup
22
+ gates.
23
+
24
+ Public docs describe the measurement method and output shape. They do not make
25
+ real-app speed claims unless equivalent candidate and baseline flows were run
26
+ under the same local conditions and the report can be shared safely.
27
+
28
+ ## Consequences
29
+
30
+ - Benchmark infrastructure is reusable without hardcoded private app details.
31
+ - App teams can compare against whatever runner they already use by wrapping a
32
+ command.
33
+ - CI can fail when a speed or reliability claim is not supported by the latest
34
+ repeated-run data.
35
+ - Public performance statements require fair inputs: same app build, same
36
+ device/simulator state, same user path, repeated runs, and trace-backed
37
+ failure diagnostics.
@@ -0,0 +1,12 @@
1
+ # Architecture Decisions
2
+
3
+ Accepted architecture decisions for ZMR are recorded here so product docs,
4
+ protocol docs, and implementation choices stay aligned.
5
+
6
+ - [0001: Agent-Native Runner Boundary](0001-agent-native-runner-boundary.md)
7
+ - [0002: App-Local `.zmr/` Contract](0002-app-local-zmr-contract.md)
8
+ - [0003: iOS XCTest Shim](0003-ios-simulator-xctest-shim.md)
9
+ - [0004: Benchmark Claims And Baseline Collection](0004-benchmark-claims-and-baseline-collection.md)
10
+
11
+ ADRs describe current decisions, not permanent constraints. A later ADR can
12
+ supersede an earlier decision when the product or support matrix changes.