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
package/src/doctor.zig ADDED
@@ -0,0 +1,412 @@
1
+ const std = @import("std");
2
+ const android = @import("android.zig");
3
+ const command = @import("command.zig");
4
+ const config = @import("config.zig");
5
+ const doctor_hints = @import("doctor_hints.zig");
6
+ const ios = @import("ios.zig");
7
+ const types = @import("types.zig");
8
+ const validation = @import("validation.zig");
9
+
10
+ pub const Status = doctor_hints.Status;
11
+
12
+ pub const Check = struct {
13
+ name: []const u8,
14
+ status: Status,
15
+ detail: []const u8,
16
+ error_code: ?[]const u8 = null,
17
+ field_path: ?[]const u8 = null,
18
+ hint: ?[]const u8 = null,
19
+ count: ?usize = null,
20
+ ready_count: ?usize = null,
21
+ script_count: ?usize = null,
22
+ script_names: ?[]const []const u8 = null,
23
+
24
+ pub fn deinit(self: Check, allocator: std.mem.Allocator) void {
25
+ allocator.free(self.name);
26
+ allocator.free(self.detail);
27
+ if (self.error_code) |error_code| allocator.free(error_code);
28
+ if (self.field_path) |field_path| allocator.free(field_path);
29
+ if (self.hint) |hint| allocator.free(hint);
30
+ if (self.script_names) |script_names| freeStringList(allocator, script_names);
31
+ }
32
+ };
33
+
34
+ pub const Options = struct {
35
+ zig_path: []const u8 = "zig",
36
+ adb_path: []const u8 = "adb",
37
+ android_shim_path: ?[]const u8 = null,
38
+ android_smoke_scenario: ?[]const u8 = null,
39
+ xcrun_path: []const u8 = "xcrun",
40
+ ios_shim_path: ?[]const u8 = null,
41
+ ios_smoke_scenario: ?[]const u8 = null,
42
+ };
43
+
44
+ pub fn checkConfigLoaded(allocator: std.mem.Allocator, path: []const u8, scripts: []const config.ScriptCommand) !Check {
45
+ const name = try allocator.dupe(u8, "config");
46
+ errdefer allocator.free(name);
47
+ const detail = try allocator.dupe(u8, path);
48
+ errdefer allocator.free(detail);
49
+ const script_names = try duplicateScriptNames(allocator, scripts);
50
+ errdefer freeStringList(allocator, script_names);
51
+
52
+ return .{
53
+ .name = name,
54
+ .status = .ok,
55
+ .detail = detail,
56
+ .script_count = scripts.len,
57
+ .script_names = script_names,
58
+ };
59
+ }
60
+
61
+ fn duplicateScriptNames(allocator: std.mem.Allocator, scripts: []const config.ScriptCommand) ![]const []const u8 {
62
+ if (scripts.len == 0) return &.{};
63
+
64
+ var names = try allocator.alloc([]const u8, scripts.len);
65
+ errdefer allocator.free(names);
66
+ var written: usize = 0;
67
+ errdefer {
68
+ for (names[0..written]) |name| allocator.free(name);
69
+ }
70
+
71
+ for (scripts, 0..) |script, index| {
72
+ names[index] = try allocator.dupe(u8, script.name);
73
+ written += 1;
74
+ }
75
+ return names;
76
+ }
77
+
78
+ fn freeStringList(allocator: std.mem.Allocator, list: []const []const u8) void {
79
+ if (list.len == 0) return;
80
+ for (list) |item| allocator.free(item);
81
+ allocator.free(list);
82
+ }
83
+
84
+ pub fn checkConfigError(allocator: std.mem.Allocator, path: []const u8, err: anyerror, field_path: ?[]const u8) !Check {
85
+ const status: Status = if (err == error.FileNotFound) .missing else .warning;
86
+ return .{
87
+ .name = try allocator.dupe(u8, "config"),
88
+ .status = status,
89
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ path, @errorName(err) }),
90
+ .error_code = try allocator.dupe(u8, configErrorCode(err)),
91
+ .field_path = if (field_path) |value| try allocator.dupe(u8, value) else null,
92
+ .hint = try doctor_hints.hintForCheck(allocator, "config", status),
93
+ };
94
+ }
95
+
96
+ fn configErrorCode(err: anyerror) []const u8 {
97
+ return switch (err) {
98
+ error.FileNotFound => "config.file_not_found",
99
+ error.ConfigMustBeObject => "config.must_be_object",
100
+ error.MissingConfigSchemaVersion => "config.missing_schema_version",
101
+ error.ConfigSchemaVersionMustBeInteger => "config.schema_version_type",
102
+ error.UnsupportedConfigVersion => "config.unsupported_version",
103
+ error.ConfigUnknownField => "config.unknown_field",
104
+ error.ConfigPlatformMustBeObject,
105
+ error.ConfigToolsMustBeObject,
106
+ error.ConfigArtifactsMustBeObject,
107
+ error.ConfigRedactionMustBeObject,
108
+ error.ConfigScriptsMustBeObject,
109
+ error.ConfigFieldMustBeBool,
110
+ error.ConfigFieldMustBeString,
111
+ error.ConfigFieldMustBeStringArray,
112
+ => "config.field_type",
113
+ error.ConfigFieldMustBeNonEmptyString => "config.empty_string",
114
+ else => "config.invalid",
115
+ };
116
+ }
117
+
118
+ pub fn run(allocator: std.mem.Allocator, options: Options) ![]Check {
119
+ var checks = std.ArrayList(Check).empty;
120
+ errdefer {
121
+ for (checks.items) |check| check.deinit(allocator);
122
+ checks.deinit(allocator);
123
+ }
124
+
125
+ try checks.append(allocator, try checkCommand(allocator, "zig", &.{ options.zig_path, "version" }));
126
+ try checks.append(allocator, try checkCommand(allocator, "adb", &.{ options.adb_path, "version" }));
127
+ try checks.append(allocator, try checkAndroidDevices(allocator, options.adb_path));
128
+ if (options.android_shim_path) |path| try checks.append(allocator, try checkPath(allocator, "android-shim", path));
129
+ if (options.android_smoke_scenario) |path| try checks.append(allocator, try checkScenarioPath(allocator, "android-smoke-scenario", path));
130
+ try checks.append(allocator, try checkCommand(allocator, "xcrun", &.{ options.xcrun_path, "--version" }));
131
+ try checks.append(allocator, try checkIosSimulators(allocator, options.xcrun_path));
132
+ try checks.append(allocator, try checkIosPhysicalDevices(allocator, options.xcrun_path));
133
+ if (options.ios_shim_path) |path| try checks.append(allocator, try checkPath(allocator, "ios-shim", path));
134
+ if (options.ios_smoke_scenario) |path| try checks.append(allocator, try checkScenarioPath(allocator, "ios-smoke-scenario", path));
135
+
136
+ return try checks.toOwnedSlice(allocator);
137
+ }
138
+
139
+ fn checkPath(allocator: std.mem.Allocator, name: []const u8, path: []const u8) !Check {
140
+ std.fs.cwd().access(path, .{ .mode = .read_only }) catch |err| {
141
+ return .{
142
+ .name = try allocator.dupe(u8, name),
143
+ .status = .missing,
144
+ .error_code = try allocator.dupe(u8, doctor_hints.setupErrorCode(name, .missing)),
145
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ path, @errorName(err) }),
146
+ .hint = try doctor_hints.hintForCheck(allocator, name, .missing),
147
+ };
148
+ };
149
+ return .{
150
+ .name = try allocator.dupe(u8, name),
151
+ .status = .ok,
152
+ .detail = try allocator.dupe(u8, path),
153
+ };
154
+ }
155
+
156
+ fn checkScenarioPath(allocator: std.mem.Allocator, name: []const u8, path: []const u8) !Check {
157
+ const result = try validation.validateFile(allocator, path);
158
+ defer result.deinit(allocator);
159
+
160
+ if (result.ok) {
161
+ return .{
162
+ .name = try allocator.dupe(u8, name),
163
+ .status = .ok,
164
+ .detail = try std.fmt.allocPrint(allocator, "{s}: ok ({s}, {d} steps)", .{ path, result.name.?, result.step_count }),
165
+ };
166
+ }
167
+
168
+ const status: Status = if (result.error_code != null and std.mem.eql(u8, result.error_code.?, "scenario.file_not_found")) .missing else .warning;
169
+ const code = result.error_code orelse "scenario.invalid";
170
+ return .{
171
+ .name = try allocator.dupe(u8, name),
172
+ .status = status,
173
+ .detail = try scenarioValidationDetail(allocator, path, result),
174
+ .error_code = try allocator.dupe(u8, code),
175
+ .hint = try doctor_hints.hintForCheck(allocator, name, status),
176
+ };
177
+ }
178
+
179
+ fn scenarioValidationDetail(allocator: std.mem.Allocator, path: []const u8, result: validation.Result) ![]const u8 {
180
+ const code = result.error_code orelse "scenario.invalid";
181
+ const message = result.message orelse "scenario is invalid";
182
+ if (result.path) |field_path| {
183
+ if (result.line) |line| {
184
+ if (result.column) |column| {
185
+ return try std.fmt.allocPrint(allocator, "{s}: invalid [{s}] {s} at {s} line {d} column {d}", .{ path, code, message, field_path, line, column });
186
+ }
187
+ }
188
+ return try std.fmt.allocPrint(allocator, "{s}: invalid [{s}] {s} at {s}", .{ path, code, message, field_path });
189
+ }
190
+ if (result.line) |line| {
191
+ if (result.column) |column| {
192
+ return try std.fmt.allocPrint(allocator, "{s}: invalid [{s}] {s} line {d} column {d}", .{ path, code, message, line, column });
193
+ }
194
+ }
195
+ return try std.fmt.allocPrint(allocator, "{s}: invalid [{s}] {s}", .{ path, code, message });
196
+ }
197
+
198
+ fn checkAndroidDevices(allocator: std.mem.Allocator, adb_path: []const u8) !Check {
199
+ const devices = android.listDevices(allocator, adb_path) catch |err| {
200
+ return .{
201
+ .name = try allocator.dupe(u8, "android-devices"),
202
+ .status = .missing,
203
+ .error_code = try allocator.dupe(u8, "setup.android.devices_unavailable"),
204
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ adb_path, @errorName(err) }),
205
+ .hint = try doctor_hints.hintForCheck(allocator, "android-devices", .missing),
206
+ };
207
+ };
208
+ defer {
209
+ for (devices) |device| device.deinit(allocator);
210
+ allocator.free(devices);
211
+ }
212
+ if (devices.len == 0) {
213
+ return .{
214
+ .name = try allocator.dupe(u8, "android-devices"),
215
+ .status = .warning,
216
+ .error_code = try allocator.dupe(u8, "setup.android.no_devices"),
217
+ .detail = try allocator.dupe(u8, "0 Android device(s)"),
218
+ .hint = try doctor_hints.hintForCheck(allocator, "android-devices", .warning),
219
+ .count = 0,
220
+ .ready_count = 0,
221
+ };
222
+ }
223
+ return .{
224
+ .name = try allocator.dupe(u8, "android-devices"),
225
+ .status = .ok,
226
+ .detail = try std.fmt.allocPrint(allocator, "{d} Android device(s)", .{devices.len}),
227
+ .count = devices.len,
228
+ .ready_count = devices.len,
229
+ };
230
+ }
231
+
232
+ fn checkIosSimulators(allocator: std.mem.Allocator, xcrun_path: []const u8) !Check {
233
+ const devices = ios.listDevices(allocator, xcrun_path) catch |err| {
234
+ return .{
235
+ .name = try allocator.dupe(u8, "ios-simulators"),
236
+ .status = .missing,
237
+ .error_code = try allocator.dupe(u8, "setup.ios.simulators_unavailable"),
238
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ xcrun_path, @errorName(err) }),
239
+ .hint = try doctor_hints.hintForCheck(allocator, "ios-simulators", .missing),
240
+ };
241
+ };
242
+ defer {
243
+ for (devices) |device| device.deinit(allocator);
244
+ allocator.free(devices);
245
+ }
246
+ if (devices.len == 0) {
247
+ return .{
248
+ .name = try allocator.dupe(u8, "ios-simulators"),
249
+ .status = .warning,
250
+ .error_code = try allocator.dupe(u8, "setup.ios.no_booted_simulators"),
251
+ .detail = try allocator.dupe(u8, "0 booted iOS simulator(s)"),
252
+ .hint = try doctor_hints.hintForCheck(allocator, "ios-simulators", .warning),
253
+ .count = 0,
254
+ .ready_count = 0,
255
+ };
256
+ }
257
+ return .{
258
+ .name = try allocator.dupe(u8, "ios-simulators"),
259
+ .status = .ok,
260
+ .detail = try std.fmt.allocPrint(allocator, "{d} booted iOS simulator(s)", .{devices.len}),
261
+ .count = devices.len,
262
+ .ready_count = devices.len,
263
+ };
264
+ }
265
+
266
+ fn checkIosPhysicalDevices(allocator: std.mem.Allocator, xcrun_path: []const u8) !Check {
267
+ const devices = ios.listPhysicalDevices(allocator, xcrun_path) catch |err| {
268
+ return .{
269
+ .name = try allocator.dupe(u8, "ios-physical-devices"),
270
+ .status = .missing,
271
+ .error_code = try allocator.dupe(u8, "setup.ios.physical_devices_unavailable"),
272
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ xcrun_path, @errorName(err) }),
273
+ .hint = try doctor_hints.hintForCheck(allocator, "ios-physical-devices", .missing),
274
+ };
275
+ };
276
+ defer {
277
+ for (devices) |device| device.deinit(allocator);
278
+ allocator.free(devices);
279
+ }
280
+ if (devices.len == 0) {
281
+ return .{
282
+ .name = try allocator.dupe(u8, "ios-physical-devices"),
283
+ .status = .warning,
284
+ .error_code = try allocator.dupe(u8, "setup.ios.no_physical_devices"),
285
+ .detail = try allocator.dupe(u8, "0 physical iOS device(s)"),
286
+ .hint = try doctor_hints.hintForCheck(allocator, "ios-physical-devices", .warning),
287
+ .count = 0,
288
+ .ready_count = 0,
289
+ };
290
+ }
291
+ var ready_count: usize = 0;
292
+ for (devices) |device| {
293
+ if (isReadyPhysicalDeviceState(device.state)) ready_count += 1;
294
+ }
295
+ if (ready_count == 0) {
296
+ const breakdown = try physicalStateBreakdown(allocator, devices);
297
+ defer allocator.free(breakdown);
298
+ return .{
299
+ .name = try allocator.dupe(u8, "ios-physical-devices"),
300
+ .status = .warning,
301
+ .error_code = try allocator.dupe(u8, "setup.ios.no_ready_physical_devices"),
302
+ .detail = try std.fmt.allocPrint(allocator, "0 ready physical iOS device(s); {d} listed{s}", .{ devices.len, breakdown }),
303
+ .hint = try doctor_hints.hintForCheck(allocator, "ios-physical-devices", .warning),
304
+ .count = devices.len,
305
+ .ready_count = 0,
306
+ };
307
+ }
308
+ if (ready_count < devices.len) {
309
+ const breakdown = try physicalStateBreakdown(allocator, devices);
310
+ defer allocator.free(breakdown);
311
+ return .{
312
+ .name = try allocator.dupe(u8, "ios-physical-devices"),
313
+ .status = .ok,
314
+ .detail = try std.fmt.allocPrint(allocator, "{d} ready physical iOS device(s); {d} listed{s}", .{ ready_count, devices.len, breakdown }),
315
+ .count = devices.len,
316
+ .ready_count = ready_count,
317
+ };
318
+ }
319
+ return .{
320
+ .name = try allocator.dupe(u8, "ios-physical-devices"),
321
+ .status = .ok,
322
+ .detail = try std.fmt.allocPrint(allocator, "{d} ready physical iOS device(s)", .{ready_count}),
323
+ .count = devices.len,
324
+ .ready_count = ready_count,
325
+ };
326
+ }
327
+
328
+ fn isReadyPhysicalDeviceState(state: []const u8) bool {
329
+ return std.mem.eql(u8, state, "connected") or std.mem.eql(u8, state, "available");
330
+ }
331
+
332
+ fn physicalStateBreakdown(allocator: std.mem.Allocator, devices: []const types.DeviceInfo) ![]const u8 {
333
+ var disconnected: usize = 0;
334
+ var paired: usize = 0;
335
+ var unavailable: usize = 0;
336
+ var other: usize = 0;
337
+
338
+ for (devices) |device| {
339
+ if (std.mem.eql(u8, device.state, "disconnected")) {
340
+ disconnected += 1;
341
+ } else if (std.mem.eql(u8, device.state, "paired")) {
342
+ paired += 1;
343
+ } else if (std.mem.eql(u8, device.state, "unavailable")) {
344
+ unavailable += 1;
345
+ } else if (!isReadyPhysicalDeviceState(device.state)) {
346
+ other += 1;
347
+ }
348
+ }
349
+
350
+ var parts = std.ArrayList(u8).empty;
351
+ defer parts.deinit(allocator);
352
+ const writer = parts.writer(allocator);
353
+ var wrote = false;
354
+ if (disconnected > 0) {
355
+ try writer.print("disconnected={d}", .{disconnected});
356
+ wrote = true;
357
+ }
358
+ if (paired > 0) {
359
+ if (wrote) try writer.writeAll(", ");
360
+ try writer.print("paired={d}", .{paired});
361
+ wrote = true;
362
+ }
363
+ if (unavailable > 0) {
364
+ if (wrote) try writer.writeAll(", ");
365
+ try writer.print("unavailable={d}", .{unavailable});
366
+ wrote = true;
367
+ }
368
+ if (other > 0) {
369
+ if (wrote) try writer.writeAll(", ");
370
+ try writer.print("other={d}", .{other});
371
+ wrote = true;
372
+ }
373
+
374
+ if (!wrote) return try allocator.dupe(u8, "");
375
+ return try std.fmt.allocPrint(allocator, " ({s})", .{parts.items});
376
+ }
377
+
378
+ pub fn checkCommand(allocator: std.mem.Allocator, name: []const u8, argv: []const []const u8) !Check {
379
+ const result = command.run(allocator, argv, 1024 * 1024) catch |err| {
380
+ return .{
381
+ .name = try allocator.dupe(u8, name),
382
+ .status = .missing,
383
+ .error_code = try allocator.dupe(u8, doctor_hints.setupErrorCode(name, .missing)),
384
+ .detail = try std.fmt.allocPrint(allocator, "{s}: {s}", .{ argv[0], @errorName(err) }),
385
+ .hint = try doctor_hints.hintForCheck(allocator, name, .missing),
386
+ };
387
+ };
388
+ defer result.deinit(allocator);
389
+
390
+ if (result.term == .Exited and result.term.Exited == 0) {
391
+ return .{
392
+ .name = try allocator.dupe(u8, name),
393
+ .status = .ok,
394
+ .detail = try firstLine(allocator, result.stdout),
395
+ };
396
+ }
397
+
398
+ const code = if (result.term == .Exited) result.term.Exited else 255;
399
+ return .{
400
+ .name = try allocator.dupe(u8, name),
401
+ .status = .warning,
402
+ .error_code = try allocator.dupe(u8, doctor_hints.setupErrorCode(name, .warning)),
403
+ .detail = try std.fmt.allocPrint(allocator, "exit {d}: {s}", .{ code, std.mem.trim(u8, result.stderr, " \t\r\n") }),
404
+ .hint = try doctor_hints.hintForCheck(allocator, name, .warning),
405
+ };
406
+ }
407
+
408
+ fn firstLine(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
409
+ const trimmed = std.mem.trim(u8, value, " \t\r\n");
410
+ const end = std.mem.indexOfScalar(u8, trimmed, '\n') orelse trimmed.len;
411
+ return try allocator.dupe(u8, trimmed[0..end]);
412
+ }
@@ -0,0 +1,52 @@
1
+ const std = @import("std");
2
+
3
+ pub const Status = enum {
4
+ ok,
5
+ warning,
6
+ missing,
7
+ };
8
+
9
+ pub fn setupErrorCode(name: []const u8, status: Status) []const u8 {
10
+ if (std.mem.eql(u8, name, "zig")) return if (status == .missing) "setup.zig.not_found" else "setup.zig.command_failed";
11
+ if (std.mem.eql(u8, name, "adb")) return if (status == .missing) "setup.adb.not_found" else "setup.adb.command_failed";
12
+ if (std.mem.eql(u8, name, "xcrun")) return if (status == .missing) "setup.xcrun.not_found" else "setup.xcrun.command_failed";
13
+ if (std.mem.eql(u8, name, "android-shim")) return if (status == .missing) "setup.android_shim.not_found" else "setup.android_shim.command_failed";
14
+ if (std.mem.eql(u8, name, "ios-shim")) return if (status == .missing) "setup.ios_shim.not_found" else "setup.ios_shim.command_failed";
15
+ return if (status == .missing) "setup.tool.not_found" else "setup.tool.command_failed";
16
+ }
17
+
18
+ pub fn hintForCheck(allocator: std.mem.Allocator, name: []const u8, status: Status) !?[]const u8 {
19
+ if (status == .ok) return null;
20
+ const hint =
21
+ if (std.mem.eql(u8, name, "zig"))
22
+ "Install Zig 0.15.2 or newer, ensure it is on PATH, then run zmr doctor again."
23
+ else if (std.mem.eql(u8, name, "adb"))
24
+ "Install Android SDK Platform Tools, ensure adb is on PATH, then run adb devices."
25
+ else if (std.mem.eql(u8, name, "android-devices"))
26
+ "Start an emulator or connect a device, confirm adb devices shows it, then pass --device when running scenarios."
27
+ else if (std.mem.eql(u8, name, "config"))
28
+ "Fix the config file or regenerate it with npx zmr-wizard, then run zmr doctor --strict --json --config .zmr/config.json."
29
+ else if (std.mem.eql(u8, name, "android-shim"))
30
+ "Run npx zmr-install-android-shim in the app repo or update tools.androidShimPath in .zmr/config.json."
31
+ else if (std.mem.eql(u8, name, "android-smoke-scenario"))
32
+ if (status == .warning)
33
+ "Run zmr validate on the configured Android smoke scenario, fix the reported issue, or update android.smokeScenario in .zmr/config.json."
34
+ else
35
+ "Run npx zmr-wizard, create the Android smoke scenario, or update android.smokeScenario in .zmr/config.json."
36
+ else if (std.mem.eql(u8, name, "xcrun"))
37
+ "Install Xcode command line tools, run xcode-select --install if needed, then run xcrun --version."
38
+ else if (std.mem.eql(u8, name, "ios-simulators"))
39
+ "Boot an iOS simulator with Xcode or xcrun simctl boot, then run xcrun simctl list devices booted."
40
+ else if (std.mem.eql(u8, name, "ios-physical-devices"))
41
+ "Connect and trust an iPhone, enable Developer Mode, confirm zmr devices --json --platform ios --ios-device-type physical reports ready:true, then pass --ios-device-type physical --device <physical-device-id>."
42
+ else if (std.mem.eql(u8, name, "ios-shim"))
43
+ "Run npx zmr-install-ios-shim in the app repo or update tools.iosShimPath in .zmr/config.json."
44
+ else if (std.mem.eql(u8, name, "ios-smoke-scenario"))
45
+ if (status == .warning)
46
+ "Run zmr validate on the configured iOS smoke scenario, fix the reported issue, or update ios.smokeScenario in .zmr/config.json."
47
+ else
48
+ "Run npx zmr-wizard, create the iOS smoke scenario, or update ios.smokeScenario in .zmr/config.json."
49
+ else
50
+ "Run the command manually, fix the reported setup issue, then run zmr doctor again.";
51
+ return try allocator.dupe(u8, hint);
52
+ }
package/src/errors.zig ADDED
@@ -0,0 +1,55 @@
1
+ const std = @import("std");
2
+
3
+ pub const PublicError = struct {
4
+ code: []const u8,
5
+ message: []const u8,
6
+ };
7
+
8
+ pub fn classify(err: anyerror) PublicError {
9
+ return switch (err) {
10
+ error.MissingScenarioPath => .{ .code = "cli.missing_scenario", .message = "missing scenario path" },
11
+ error.MissingDeviceSerial => .{ .code = "cli.missing_device", .message = "missing device serial" },
12
+ error.MissingTraceDir => .{ .code = "cli.missing_trace_dir", .message = "missing trace directory" },
13
+ error.MissingAppId => .{ .code = "cli.missing_app_id", .message = "missing app id" },
14
+ error.MissingAdbPath => .{ .code = "cli.missing_adb_path", .message = "missing adb path" },
15
+ error.MissingXcrunPath => .{ .code = "cli.missing_xcrun_path", .message = "missing xcrun path" },
16
+ error.MissingZigPath => .{ .code = "cli.missing_zig_path", .message = "missing zig path" },
17
+ error.MissingPlatform => .{ .code = "cli.missing_platform", .message = "missing platform" },
18
+ error.UnknownCommand => .{ .code = "cli.unknown_command", .message = "unknown command" },
19
+ error.UnknownFlag => .{ .code = "cli.unknown_flag", .message = "unknown flag" },
20
+ error.MissingParam => .{ .code = "cli.missing_param", .message = "missing parameter" },
21
+ error.FileNotFound => .{ .code = "scenario.file_not_found", .message = "scenario file was not found" },
22
+ error.UnsupportedPlatform => .{ .code = "cli.unsupported_platform", .message = "unsupported platform" },
23
+ error.UnsupportedTransport => .{ .code = "cli.unsupported_transport", .message = "unsupported transport" },
24
+ error.ScenarioMustBeObject,
25
+ error.ScenarioMissingSteps,
26
+ error.ScenarioStepsMustBeArray,
27
+ error.StepMustBeObject,
28
+ error.StepMissingAction,
29
+ error.StepActionMustBeString,
30
+ error.UnknownAction,
31
+ error.UnknownScenarioAction,
32
+ error.UnknownScrollDirection,
33
+ error.StepMissingUrl,
34
+ error.StepMissingText,
35
+ error.StepMissingX1,
36
+ error.StepMissingY1,
37
+ error.StepMissingX2,
38
+ error.StepMissingY2,
39
+ => .{ .code = "scenario.invalid", .message = "scenario is invalid" },
40
+ error.SelectorMustNotBeEmpty,
41
+ error.MissingSelector,
42
+ error.StepMissingSelector,
43
+ error.MissingSelectors,
44
+ error.StepMissingSelectors,
45
+ error.SelectorsMustBeArray,
46
+ error.SelectorsMustNotBeEmpty,
47
+ => .{ .code = "selector.invalid", .message = "selector is invalid" },
48
+ error.WaitTimeout => .{ .code = "runner.wait_timeout", .message = "wait timed out" },
49
+ error.AssertionFailed => .{ .code = "runner.assertion_failed", .message = "assertion failed" },
50
+ error.SelectorNotFound => .{ .code = "runner.selector_not_found", .message = "selector not found" },
51
+ error.CommandFailed => .{ .code = "device.command_failed", .message = "device command failed" },
52
+ error.IosXCTestShimRequired => .{ .code = "ios.xctest_shim_required", .message = "iOS selector interaction requires the XCTest shim" },
53
+ else => .{ .code = "internal.error", .message = @errorName(err) },
54
+ };
55
+ }
@@ -0,0 +1,163 @@
1
+ const std = @import("std");
2
+ const types = @import("types.zig");
3
+
4
+ pub const FakeDevice = struct {
5
+ allocator: std.mem.Allocator,
6
+ snapshots: []types.ObservationSnapshot,
7
+ snapshot_index: usize = 0,
8
+ taps: usize = 0,
9
+ swipes: usize = 0,
10
+ erases: usize = 0,
11
+ hides_keyboard: usize = 0,
12
+ presses_back: usize = 0,
13
+ stopped: bool = false,
14
+ cleared: bool = false,
15
+ last_swipe: ?SwipeRecord = null,
16
+ last_erase_chars: u32 = 0,
17
+ typed_text: std.ArrayList([]const u8),
18
+ launched: bool = false,
19
+ installed_path: ?[]const u8 = null,
20
+ opened_link: ?[]const u8 = null,
21
+ settles: usize = 0,
22
+ last_settle_timeout_ms: u64 = 0,
23
+
24
+ pub fn init(allocator: std.mem.Allocator, snapshots: []types.ObservationSnapshot) FakeDevice {
25
+ return .{
26
+ .allocator = allocator,
27
+ .snapshots = snapshots,
28
+ .typed_text = std.ArrayList([]const u8).empty,
29
+ };
30
+ }
31
+
32
+ pub fn deinit(self: *FakeDevice) void {
33
+ for (self.typed_text.items) |text| self.allocator.free(text);
34
+ self.typed_text.deinit(self.allocator);
35
+ if (self.installed_path) |value| self.allocator.free(value);
36
+ if (self.opened_link) |value| self.allocator.free(value);
37
+ }
38
+
39
+ pub fn install(self: *FakeDevice, apk_path: []const u8) !void {
40
+ if (self.installed_path) |value| self.allocator.free(value);
41
+ self.installed_path = try self.allocator.dupe(u8, apk_path);
42
+ }
43
+
44
+ pub fn launch(self: *FakeDevice) !void {
45
+ self.launched = true;
46
+ }
47
+
48
+ pub fn stop(self: *FakeDevice) !void {
49
+ self.stopped = true;
50
+ }
51
+
52
+ pub fn clearState(self: *FakeDevice) !void {
53
+ self.cleared = true;
54
+ }
55
+
56
+ pub fn listDevices(self: *FakeDevice) ![]types.DeviceInfo {
57
+ const devices = try self.allocator.alloc(types.DeviceInfo, 1);
58
+ errdefer self.allocator.free(devices);
59
+ const serial = try self.allocator.dupe(u8, "fake-device-1");
60
+ errdefer self.allocator.free(serial);
61
+ const state = try self.allocator.dupe(u8, "device");
62
+ devices[0] = .{
63
+ .serial = serial,
64
+ .state = state,
65
+ };
66
+ return devices;
67
+ }
68
+
69
+ pub fn openLink(self: *FakeDevice, url: []const u8) !void {
70
+ if (self.opened_link) |value| self.allocator.free(value);
71
+ self.opened_link = try self.allocator.dupe(u8, url);
72
+ }
73
+
74
+ pub fn tap(self: *FakeDevice, x: i32, y: i32) !void {
75
+ _ = x;
76
+ _ = y;
77
+ self.taps += 1;
78
+ }
79
+
80
+ pub fn typeText(self: *FakeDevice, text: []const u8) !void {
81
+ try self.typed_text.append(self.allocator, try self.allocator.dupe(u8, text));
82
+ }
83
+
84
+ pub fn eraseText(self: *FakeDevice, max_chars: u32) !void {
85
+ self.last_erase_chars = max_chars;
86
+ self.erases += 1;
87
+ }
88
+
89
+ pub fn hideKeyboard(self: *FakeDevice) !void {
90
+ self.hides_keyboard += 1;
91
+ }
92
+
93
+ pub fn swipe(self: *FakeDevice, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !void {
94
+ self.swipes += 1;
95
+ self.last_swipe = .{
96
+ .x1 = x1,
97
+ .y1 = y1,
98
+ .x2 = x2,
99
+ .y2 = y2,
100
+ .duration_ms = duration_ms,
101
+ };
102
+ }
103
+
104
+ pub fn pressBack(self: *FakeDevice) !void {
105
+ self.presses_back += 1;
106
+ }
107
+
108
+ pub fn settle(self: *FakeDevice, timeout_ms: u64) !void {
109
+ self.settles += 1;
110
+ self.last_settle_timeout_ms = timeout_ms;
111
+ }
112
+
113
+ pub fn snapshot(self: *FakeDevice, writer: anytype) !types.ObservationSnapshot {
114
+ _ = writer;
115
+ if (self.snapshots.len == 0) return error.NoFakeSnapshots;
116
+ const index = @min(self.snapshot_index, self.snapshots.len - 1);
117
+ if (self.snapshot_index + 1 < self.snapshots.len) self.snapshot_index += 1;
118
+ return try cloneSnapshot(self.allocator, self.snapshots[index]);
119
+ }
120
+ };
121
+
122
+ pub const SwipeRecord = struct {
123
+ x1: i32,
124
+ y1: i32,
125
+ x2: i32,
126
+ y2: i32,
127
+ duration_ms: u32,
128
+ };
129
+
130
+ pub fn cloneSnapshot(allocator: std.mem.Allocator, source: types.ObservationSnapshot) !types.ObservationSnapshot {
131
+ var nodes = try allocator.alloc(types.UiNode, source.nodes.len);
132
+ errdefer allocator.free(nodes);
133
+ var initialized: usize = 0;
134
+ errdefer {
135
+ for (nodes[0..initialized]) |node| node.deinit(allocator);
136
+ }
137
+ for (source.nodes, 0..) |node, index| {
138
+ nodes[index] = .{
139
+ .stable_id = try allocator.dupe(u8, node.stable_id),
140
+ .class_name = try allocator.dupe(u8, node.class_name),
141
+ .resource_id = try types.dupeOptional(allocator, node.resource_id),
142
+ .text = try types.dupeOptional(allocator, node.text),
143
+ .content_desc = try types.dupeOptional(allocator, node.content_desc),
144
+ .bounds = node.bounds,
145
+ .enabled = node.enabled,
146
+ .visible = node.visible,
147
+ .selected = node.selected,
148
+ };
149
+ initialized += 1;
150
+ }
151
+ return .{
152
+ .id = try allocator.dupe(u8, source.id),
153
+ .timestamp_ms = source.timestamp_ms,
154
+ .viewport = source.viewport,
155
+ .active_package = try types.dupeOptional(allocator, source.active_package),
156
+ .active_activity = try types.dupeOptional(allocator, source.active_activity),
157
+ .screenshot_artifact = try types.dupeOptional(allocator, source.screenshot_artifact),
158
+ .tree_artifact = try types.dupeOptional(allocator, source.tree_artifact),
159
+ .focused_node_id = try types.dupeOptional(allocator, source.focused_node_id),
160
+ .log_delta = try types.dupeOptional(allocator, source.log_delta),
161
+ .nodes = nodes,
162
+ };
163
+ }