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,259 @@
1
+ const std = @import("std");
2
+
3
+ const android = @import("android.zig");
4
+ const android_emulator = @import("android_emulator.zig");
5
+ const cli_output = @import("cli_output.zig");
6
+ const config_paths = @import("config_paths.zig");
7
+ const ios = @import("ios.zig");
8
+ const runner = @import("runner.zig");
9
+ const run_options = @import("run_options.zig");
10
+ const scenario = @import("scenario.zig");
11
+ const trace = @import("trace.zig");
12
+
13
+ pub const ParsedArgs = struct {
14
+ raw: run_options.RawRunOptions = .{},
15
+ adb_path: []const u8 = "adb",
16
+ emulator_path: []const u8 = "emulator",
17
+ avdmanager_path: []const u8 = "avdmanager",
18
+ xcrun_path: []const u8 = "xcrun",
19
+ adb_path_set: bool = false,
20
+ emulator_path_set: bool = false,
21
+ avdmanager_path_set: bool = false,
22
+ xcrun_path_set: bool = false,
23
+ config_path: ?[]const u8 = null,
24
+ json: bool = false,
25
+ };
26
+
27
+ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
28
+ var parsed = ParsedArgs{};
29
+ var index: usize = 0;
30
+ while (index < args.len) : (index += 1) {
31
+ const arg = args[index];
32
+ if (std.mem.eql(u8, arg, "--device")) {
33
+ index += 1;
34
+ parsed.raw.serial = if (index < args.len) args[index] else return error.MissingDeviceSerial;
35
+ } else if (std.mem.eql(u8, arg, "--trace-dir")) {
36
+ index += 1;
37
+ parsed.raw.trace_dir = if (index < args.len) args[index] else return error.MissingTraceDir;
38
+ } else if (std.mem.eql(u8, arg, "--app-id")) {
39
+ index += 1;
40
+ parsed.raw.app_id = if (index < args.len) args[index] else return error.MissingAppId;
41
+ } else if (std.mem.eql(u8, arg, "--adb")) {
42
+ index += 1;
43
+ parsed.adb_path = if (index < args.len) args[index] else return error.MissingAdbPath;
44
+ parsed.adb_path_set = true;
45
+ } else if (std.mem.eql(u8, arg, "--emulator")) {
46
+ index += 1;
47
+ parsed.emulator_path = if (index < args.len) args[index] else return error.MissingEmulatorPath;
48
+ parsed.emulator_path_set = true;
49
+ } else if (std.mem.eql(u8, arg, "--avdmanager")) {
50
+ index += 1;
51
+ parsed.avdmanager_path = if (index < args.len) args[index] else return error.MissingAvdmanagerPath;
52
+ parsed.avdmanager_path_set = true;
53
+ } else if (std.mem.eql(u8, arg, "--android-shim")) {
54
+ index += 1;
55
+ parsed.raw.android_shim_path = if (index < args.len) args[index] else return error.MissingAndroidShimPath;
56
+ } else if (std.mem.eql(u8, arg, "--xcrun")) {
57
+ index += 1;
58
+ parsed.xcrun_path = if (index < args.len) args[index] else return error.MissingXcrunPath;
59
+ parsed.xcrun_path_set = true;
60
+ } else if (std.mem.eql(u8, arg, "--ios-shim")) {
61
+ index += 1;
62
+ parsed.raw.ios_shim_path = if (index < args.len) args[index] else return error.MissingIosShimPath;
63
+ } else if (std.mem.eql(u8, arg, "--platform")) {
64
+ index += 1;
65
+ parsed.raw.platform = try parsePlatform(if (index < args.len) args[index] else return error.MissingPlatform);
66
+ } else if (std.mem.eql(u8, arg, "--ios-device-type")) {
67
+ index += 1;
68
+ parsed.raw.ios_device_type = try parseIosDeviceType(if (index < args.len) args[index] else return error.MissingIosDeviceType);
69
+ } else if (std.mem.eql(u8, arg, "--config")) {
70
+ index += 1;
71
+ parsed.config_path = if (index < args.len) args[index] else return error.MissingConfigPath;
72
+ } else if (std.mem.eql(u8, arg, "--screen-record")) {
73
+ parsed.raw.screen_recording = true;
74
+ } else if (std.mem.eql(u8, arg, "--no-screen-record")) {
75
+ parsed.raw.screen_recording = false;
76
+ } else if (std.mem.eql(u8, arg, "--android-avd")) {
77
+ index += 1;
78
+ parsed.raw.android_avd_name = if (index < args.len) args[index] else return error.MissingAndroidAvdName;
79
+ } else if (std.mem.eql(u8, arg, "--restore-snapshot")) {
80
+ index += 1;
81
+ parsed.raw.android_restore_snapshot = if (index < args.len) args[index] else return error.MissingAndroidSnapshotName;
82
+ } else if (std.mem.eql(u8, arg, "--create-avd-if-missing")) {
83
+ parsed.raw.android_create_avd_if_missing = true;
84
+ } else if (std.mem.eql(u8, arg, "--avd-system-image")) {
85
+ index += 1;
86
+ parsed.raw.android_avd_system_image = if (index < args.len) args[index] else return error.MissingAndroidAvdSystemImage;
87
+ } else if (std.mem.eql(u8, arg, "--avd-device")) {
88
+ index += 1;
89
+ parsed.raw.android_avd_device_profile = if (index < args.len) args[index] else return error.MissingAndroidAvdDeviceProfile;
90
+ } else if (std.mem.eql(u8, arg, "--reset-emulator")) {
91
+ parsed.raw.android_reset_before_run = true;
92
+ } else if (std.mem.eql(u8, arg, "--wait-emulator")) {
93
+ parsed.raw.android_wait_ready = true;
94
+ } else if (std.mem.eql(u8, arg, "--json")) {
95
+ parsed.json = true;
96
+ } else if (std.mem.startsWith(u8, arg, "--")) {
97
+ return error.UnknownFlag;
98
+ } else if (parsed.raw.scenario_path == null) {
99
+ parsed.raw.scenario_path = arg;
100
+ } else {
101
+ return error.UnknownFlag;
102
+ }
103
+ }
104
+ return parsed;
105
+ }
106
+
107
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
108
+ var raw_args = std.ArrayList([]const u8).empty;
109
+ defer raw_args.deinit(allocator);
110
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
111
+
112
+ const parsed = try parseArgs(raw_args.items);
113
+ const raw = parsed.raw;
114
+ var adb_path = parsed.adb_path;
115
+ var emulator_path = parsed.emulator_path;
116
+ var avdmanager_path = parsed.avdmanager_path;
117
+ var xcrun_path = parsed.xcrun_path;
118
+
119
+ const actual_config_path = parsed.config_path orelse config_paths.default_path;
120
+ var owned_config_paths = std.ArrayList([]const u8).empty;
121
+ defer {
122
+ for (owned_config_paths.items) |path| allocator.free(path);
123
+ owned_config_paths.deinit(allocator);
124
+ }
125
+ var config_root: ?[]const u8 = null;
126
+ defer if (config_root) |root| allocator.free(root);
127
+
128
+ var loaded_config = try config_paths.loadIfPresent(allocator, parsed.config_path);
129
+ defer if (loaded_config) |*cfg| cfg.deinit(allocator);
130
+ if (loaded_config) |cfg| {
131
+ config_root = try config_paths.rootForPath(allocator, actual_config_path);
132
+ if (!parsed.adb_path_set) {
133
+ if (cfg.tools.adb_path) |path| adb_path = try config_paths.ownCommandPath(allocator, &owned_config_paths, config_root.?, path);
134
+ }
135
+ if (!parsed.emulator_path_set) {
136
+ if (cfg.tools.emulator_path) |path| emulator_path = try config_paths.ownCommandPath(allocator, &owned_config_paths, config_root.?, path);
137
+ }
138
+ if (!parsed.avdmanager_path_set) {
139
+ if (cfg.tools.avdmanager_path) |path| avdmanager_path = try config_paths.ownCommandPath(allocator, &owned_config_paths, config_root.?, path);
140
+ }
141
+ if (!parsed.xcrun_path_set) {
142
+ if (cfg.tools.xcrun_path) |path| xcrun_path = try config_paths.ownCommandPath(allocator, &owned_config_paths, config_root.?, path);
143
+ }
144
+ }
145
+ const resolved = if (loaded_config) |cfg| run_options.resolveRun(raw, cfg) else run_options.resolveRun(raw, null);
146
+ var capture = if (loaded_config) |cfg| run_options.traceCapture(cfg) else trace.CaptureOptions{};
147
+ if (raw.screen_recording) |enabled| capture.capture_screen_recording = enabled;
148
+ const scenario_path = if (raw.scenario_path == null and config_root != null and resolved.scenario_path != null)
149
+ try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.scenario_path.?)
150
+ else
151
+ resolved.scenario_path orelse return error.MissingScenarioPath;
152
+ const trace_dir = if (raw.trace_dir == null and config_root != null and resolved.trace_dir != null)
153
+ try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.trace_dir.?)
154
+ else
155
+ resolved.trace_dir;
156
+ const android_shim_path = if (raw.android_shim_path == null and config_root != null and resolved.android_shim_path != null)
157
+ try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.android_shim_path.?)
158
+ else
159
+ resolved.android_shim_path;
160
+ const ios_shim_path = if (raw.ios_shim_path == null and config_root != null and resolved.ios_shim_path != null)
161
+ try config_paths.ownFilePath(allocator, &owned_config_paths, config_root.?, resolved.ios_shim_path.?)
162
+ else
163
+ resolved.ios_shim_path;
164
+
165
+ const script = try scenario.parseFile(allocator, scenario_path);
166
+ defer script.deinit(allocator);
167
+ const app_id = if (raw.app_id) |_| resolved.app_id else script.app_id orelse resolved.app_id;
168
+
169
+ const run_error: ?anyerror = blk: {
170
+ switch (resolved.platform) {
171
+ .android => {
172
+ if (run_options.androidPreflight(resolved, adb_path, emulator_path, avdmanager_path)) |preflight| {
173
+ try android_emulator.runPreflight(allocator, preflight);
174
+ }
175
+ var device = try android.AndroidDevice.initWithShim(allocator, adb_path, resolved.serial, app_id, android_shim_path);
176
+ defer device.deinit();
177
+ runAndroidWithTrace(allocator, &device, script, trace_dir, capture) catch |err| break :blk err;
178
+ },
179
+ .ios => {
180
+ var device = try ios.IosDevice.initWithKindAndShim(allocator, xcrun_path, resolved.serial, app_id, iosTargetKind(resolved.ios_device_type), ios_shim_path);
181
+ defer device.deinit();
182
+ runWithTrace(allocator, &device, script, trace_dir, capture) catch |err| break :blk err;
183
+ },
184
+ }
185
+ break :blk null;
186
+ };
187
+
188
+ if (parsed.json) try cli_output.writeRunSummaryJson(
189
+ allocator,
190
+ std.fs.File.stdout().deprecatedWriter(),
191
+ trace_dir,
192
+ script.name,
193
+ app_id,
194
+ run_error,
195
+ );
196
+ if (run_error) |err| return err;
197
+ }
198
+
199
+ fn runAndroidWithTrace(
200
+ allocator: std.mem.Allocator,
201
+ device: *android.AndroidDevice,
202
+ script: scenario.Scenario,
203
+ trace_dir: ?[]const u8,
204
+ capture: trace.CaptureOptions,
205
+ ) !void {
206
+ if (trace_dir == null or !capture.capture_screen_recording) {
207
+ return try runWithTrace(allocator, device, script, trace_dir, capture);
208
+ }
209
+
210
+ var trace_writer = try trace.TraceWriter.initWithOptions(allocator, trace_dir.?, capture);
211
+ defer trace_writer.deinit();
212
+
213
+ var recording = device.startScreenRecording("/sdcard/zmr-trace-screenrecord.mp4") catch null;
214
+ defer if (recording) |*rec| {
215
+ if (rec.stopAndPull(&trace_writer, "screenrecord.mp4")) |artifact_path| {
216
+ allocator.free(artifact_path);
217
+ trace_writer.recordEvent("trace.screenRecording", "{\"artifact\":\"artifacts/screenrecord.mp4\"}") catch {};
218
+ } else |_| {}
219
+ rec.deinit();
220
+ };
221
+
222
+ return try runner.runScenario(allocator, device, script, &trace_writer, .{});
223
+ }
224
+
225
+ fn runWithTrace(
226
+ allocator: std.mem.Allocator,
227
+ device: anytype,
228
+ script: scenario.Scenario,
229
+ trace_dir: ?[]const u8,
230
+ capture: trace.CaptureOptions,
231
+ ) !void {
232
+ var trace_writer: ?trace.TraceWriter = null;
233
+ if (trace_dir) |dir| {
234
+ trace_writer = try trace.TraceWriter.initWithOptions(allocator, dir, capture);
235
+ }
236
+ defer if (trace_writer) |*tw| tw.deinit();
237
+
238
+ if (trace_writer) |*tw| return try runner.runScenario(allocator, device, script, tw, .{});
239
+ return try runner.runScenario(allocator, device, script, null, .{});
240
+ }
241
+
242
+ pub fn parsePlatform(value: []const u8) !run_options.Platform {
243
+ if (std.mem.eql(u8, value, "android")) return .android;
244
+ if (std.mem.eql(u8, value, "ios")) return .ios;
245
+ return error.UnsupportedPlatform;
246
+ }
247
+
248
+ fn parseIosDeviceType(value: []const u8) !run_options.IosDeviceType {
249
+ if (std.mem.eql(u8, value, "simulator")) return .simulator;
250
+ if (std.mem.eql(u8, value, "physical")) return .physical;
251
+ return error.UnsupportedIosDeviceType;
252
+ }
253
+
254
+ fn iosTargetKind(value: run_options.IosDeviceType) ios.TargetKind {
255
+ return switch (value) {
256
+ .simulator => .simulator,
257
+ .physical => .physical,
258
+ };
259
+ }
@@ -0,0 +1,287 @@
1
+ const std = @import("std");
2
+
3
+ const android = @import("android.zig");
4
+ const config = @import("config.zig");
5
+ const config_paths = @import("config_paths.zig");
6
+ const ios = @import("ios.zig");
7
+ const json_rpc = @import("json_rpc.zig");
8
+ const mcp = @import("mcp.zig");
9
+ const run_options = @import("run_options.zig");
10
+ const trace = @import("trace.zig");
11
+
12
+ pub const ServeArgs = struct {
13
+ raw: run_options.RawServeOptions = .{},
14
+ adb_path: []const u8 = "adb",
15
+ xcrun_path: []const u8 = "xcrun",
16
+ transport: []const u8 = "stdio",
17
+ port: u16 = 8765,
18
+ config_path: ?[]const u8 = null,
19
+ };
20
+
21
+ pub const McpArgs = struct {
22
+ raw: run_options.RawServeOptions = .{},
23
+ adb_path: []const u8 = "adb",
24
+ xcrun_path: []const u8 = "xcrun",
25
+ config_path: ?[]const u8 = null,
26
+ };
27
+
28
+ const ResolvedContext = struct {
29
+ resolved: run_options.ResolvedServeOptions,
30
+ adb_path: []const u8,
31
+ xcrun_path: []const u8,
32
+ trace_dir: ?[]const u8,
33
+ android_shim_path: ?[]const u8,
34
+ ios_shim_path: ?[]const u8,
35
+ capture: trace.CaptureOptions,
36
+ };
37
+
38
+ pub fn parseServeArgs(args: []const []const u8) !ServeArgs {
39
+ var parsed = ServeArgs{};
40
+ var index: usize = 0;
41
+ while (index < args.len) : (index += 1) {
42
+ const arg = args[index];
43
+ if (std.mem.eql(u8, arg, "--transport")) {
44
+ index += 1;
45
+ parsed.transport = if (index < args.len) args[index] else return error.MissingTransport;
46
+ if (!std.mem.eql(u8, parsed.transport, "stdio") and !std.mem.eql(u8, parsed.transport, "tcp")) {
47
+ return error.UnsupportedTransport;
48
+ }
49
+ } else if (std.mem.eql(u8, arg, "--port")) {
50
+ index += 1;
51
+ const value = if (index < args.len) args[index] else return error.MissingPort;
52
+ parsed.port = try std.fmt.parseInt(u16, value, 10);
53
+ } else {
54
+ try parseCommonArg(args, &index, &parsed.raw, &parsed.adb_path, &parsed.xcrun_path, &parsed.config_path);
55
+ }
56
+ }
57
+ return parsed;
58
+ }
59
+
60
+ pub fn parseMcpArgs(args: []const []const u8) !McpArgs {
61
+ var parsed = McpArgs{};
62
+ var index: usize = 0;
63
+ while (index < args.len) : (index += 1) {
64
+ try parseCommonArg(args, &index, &parsed.raw, &parsed.adb_path, &parsed.xcrun_path, &parsed.config_path);
65
+ }
66
+ return parsed;
67
+ }
68
+
69
+ pub fn runServe(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
70
+ var raw_args = std.ArrayList([]const u8).empty;
71
+ defer raw_args.deinit(allocator);
72
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
73
+
74
+ const parsed = try parseServeArgs(raw_args.items);
75
+ var owned_config_paths = std.ArrayList([]const u8).empty;
76
+ defer {
77
+ for (owned_config_paths.items) |path| allocator.free(path);
78
+ owned_config_paths.deinit(allocator);
79
+ }
80
+ var config_root: ?[]const u8 = null;
81
+ defer if (config_root) |root| allocator.free(root);
82
+
83
+ var loaded_config = try config_paths.loadIfPresent(allocator, parsed.config_path);
84
+ defer if (loaded_config) |*cfg| cfg.deinit(allocator);
85
+
86
+ const context = try resolveContext(allocator, parsed.raw, parsed.adb_path, parsed.xcrun_path, parsed.config_path, loaded_config, &owned_config_paths, &config_root);
87
+ switch (context.resolved.platform) {
88
+ .android => {
89
+ var device = try android.AndroidDevice.initWithShim(allocator, context.adb_path, context.resolved.serial, context.resolved.app_id, context.android_shim_path);
90
+ defer device.deinit();
91
+ try serveWithDevice(allocator, &device, parsed.transport, parsed.port, context.trace_dir, context.resolved.app_id, context.capture);
92
+ },
93
+ .ios => {
94
+ var device = try ios.IosDevice.initWithKindAndShim(allocator, context.xcrun_path, context.resolved.serial, context.resolved.app_id, iosTargetKind(context.resolved.ios_device_type), context.ios_shim_path);
95
+ defer device.deinit();
96
+ try serveWithDevice(allocator, &device, parsed.transport, parsed.port, context.trace_dir, context.resolved.app_id, context.capture);
97
+ },
98
+ }
99
+ }
100
+
101
+ pub fn runMcp(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
102
+ var raw_args = std.ArrayList([]const u8).empty;
103
+ defer raw_args.deinit(allocator);
104
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
105
+
106
+ const parsed = try parseMcpArgs(raw_args.items);
107
+ var owned_config_paths = std.ArrayList([]const u8).empty;
108
+ defer {
109
+ for (owned_config_paths.items) |path| allocator.free(path);
110
+ owned_config_paths.deinit(allocator);
111
+ }
112
+ var config_root: ?[]const u8 = null;
113
+ defer if (config_root) |root| allocator.free(root);
114
+
115
+ var loaded_config = try config_paths.loadIfPresent(allocator, parsed.config_path);
116
+ defer if (loaded_config) |*cfg| cfg.deinit(allocator);
117
+
118
+ const context = try resolveContext(allocator, parsed.raw, parsed.adb_path, parsed.xcrun_path, parsed.config_path, loaded_config, &owned_config_paths, &config_root);
119
+ switch (context.resolved.platform) {
120
+ .android => {
121
+ var device = try android.AndroidDevice.initWithShim(allocator, context.adb_path, context.resolved.serial, context.resolved.app_id, context.android_shim_path);
122
+ defer device.deinit();
123
+ try serveMcpWithDevice(allocator, &device, context.trace_dir, context.resolved.app_id, context.capture);
124
+ },
125
+ .ios => {
126
+ var device = try ios.IosDevice.initWithKindAndShim(allocator, context.xcrun_path, context.resolved.serial, context.resolved.app_id, iosTargetKind(context.resolved.ios_device_type), context.ios_shim_path);
127
+ defer device.deinit();
128
+ try serveMcpWithDevice(allocator, &device, context.trace_dir, context.resolved.app_id, context.capture);
129
+ },
130
+ }
131
+ }
132
+
133
+ fn parseCommonArg(
134
+ args: []const []const u8,
135
+ index: *usize,
136
+ raw: *run_options.RawServeOptions,
137
+ adb_path: *[]const u8,
138
+ xcrun_path: *[]const u8,
139
+ config_path: *?[]const u8,
140
+ ) !void {
141
+ const arg = args[index.*];
142
+ if (std.mem.eql(u8, arg, "--device")) {
143
+ index.* += 1;
144
+ raw.serial = if (index.* < args.len) args[index.*] else return error.MissingDeviceSerial;
145
+ } else if (std.mem.eql(u8, arg, "--app-id")) {
146
+ index.* += 1;
147
+ raw.app_id = if (index.* < args.len) args[index.*] else return error.MissingAppId;
148
+ } else if (std.mem.eql(u8, arg, "--trace-dir")) {
149
+ index.* += 1;
150
+ raw.trace_dir = if (index.* < args.len) args[index.*] else return error.MissingTraceDir;
151
+ } else if (std.mem.eql(u8, arg, "--adb")) {
152
+ index.* += 1;
153
+ adb_path.* = if (index.* < args.len) args[index.*] else return error.MissingAdbPath;
154
+ } else if (std.mem.eql(u8, arg, "--android-shim")) {
155
+ index.* += 1;
156
+ raw.android_shim_path = if (index.* < args.len) args[index.*] else return error.MissingAndroidShimPath;
157
+ } else if (std.mem.eql(u8, arg, "--xcrun")) {
158
+ index.* += 1;
159
+ xcrun_path.* = if (index.* < args.len) args[index.*] else return error.MissingXcrunPath;
160
+ } else if (std.mem.eql(u8, arg, "--ios-shim")) {
161
+ index.* += 1;
162
+ raw.ios_shim_path = if (index.* < args.len) args[index.*] else return error.MissingIosShimPath;
163
+ } else if (std.mem.eql(u8, arg, "--platform")) {
164
+ index.* += 1;
165
+ raw.platform = try parsePlatform(if (index.* < args.len) args[index.*] else return error.MissingPlatform);
166
+ } else if (std.mem.eql(u8, arg, "--ios-device-type")) {
167
+ index.* += 1;
168
+ raw.ios_device_type = try parseIosDeviceType(if (index.* < args.len) args[index.*] else return error.MissingIosDeviceType);
169
+ } else if (std.mem.eql(u8, arg, "--config")) {
170
+ index.* += 1;
171
+ config_path.* = if (index.* < args.len) args[index.*] else return error.MissingConfigPath;
172
+ } else {
173
+ return error.UnknownFlag;
174
+ }
175
+ }
176
+
177
+ fn resolveContext(
178
+ allocator: std.mem.Allocator,
179
+ raw: run_options.RawServeOptions,
180
+ explicit_adb_path: []const u8,
181
+ explicit_xcrun_path: []const u8,
182
+ config_path: ?[]const u8,
183
+ loaded_config: ?config.Config,
184
+ owned_config_paths: *std.ArrayList([]const u8),
185
+ config_root: *?[]const u8,
186
+ ) !ResolvedContext {
187
+ var adb_path = explicit_adb_path;
188
+ var xcrun_path = explicit_xcrun_path;
189
+ const actual_config_path = config_path orelse config_paths.default_path;
190
+
191
+ if (loaded_config) |cfg| {
192
+ config_root.* = try config_paths.rootForPath(allocator, actual_config_path);
193
+ if (std.mem.eql(u8, adb_path, "adb")) {
194
+ if (cfg.tools.adb_path) |path| adb_path = try config_paths.ownCommandPath(allocator, owned_config_paths, config_root.*.?, path);
195
+ }
196
+ if (std.mem.eql(u8, xcrun_path, "xcrun")) {
197
+ if (cfg.tools.xcrun_path) |path| xcrun_path = try config_paths.ownCommandPath(allocator, owned_config_paths, config_root.*.?, path);
198
+ }
199
+ }
200
+
201
+ const resolved = if (loaded_config) |cfg| run_options.resolveServe(raw, cfg) else run_options.resolveServe(raw, null);
202
+ const capture = if (loaded_config) |cfg| run_options.traceCapture(cfg) else trace.CaptureOptions{};
203
+ const trace_dir = if (raw.trace_dir == null and config_root.* != null and resolved.trace_dir != null)
204
+ try config_paths.ownFilePath(allocator, owned_config_paths, config_root.*.?, resolved.trace_dir.?)
205
+ else
206
+ resolved.trace_dir;
207
+ const android_shim_path = if (raw.android_shim_path == null and config_root.* != null and resolved.android_shim_path != null)
208
+ try config_paths.ownFilePath(allocator, owned_config_paths, config_root.*.?, resolved.android_shim_path.?)
209
+ else
210
+ resolved.android_shim_path;
211
+ const ios_shim_path = if (raw.ios_shim_path == null and config_root.* != null and resolved.ios_shim_path != null)
212
+ try config_paths.ownFilePath(allocator, owned_config_paths, config_root.*.?, resolved.ios_shim_path.?)
213
+ else
214
+ resolved.ios_shim_path;
215
+
216
+ return .{
217
+ .resolved = resolved,
218
+ .adb_path = adb_path,
219
+ .xcrun_path = xcrun_path,
220
+ .trace_dir = trace_dir,
221
+ .android_shim_path = android_shim_path,
222
+ .ios_shim_path = ios_shim_path,
223
+ .capture = capture,
224
+ };
225
+ }
226
+
227
+ fn serveMcpWithDevice(
228
+ allocator: std.mem.Allocator,
229
+ device: anytype,
230
+ trace_dir: ?[]const u8,
231
+ app_id: []const u8,
232
+ capture: trace.CaptureOptions,
233
+ ) !void {
234
+ var trace_writer: ?trace.TraceWriter = null;
235
+ if (trace_dir) |dir| {
236
+ trace_writer = try trace.TraceWriter.initWithOptions(allocator, dir, capture);
237
+ try trace_writer.?.startManifest("mcp session", app_id);
238
+ }
239
+ defer if (trace_writer) |*tw| tw.deinit();
240
+ const live_trace = if (trace_writer) |*tw| tw else null;
241
+ try mcp.serveStdioWithTrace(allocator, device, live_trace);
242
+ }
243
+
244
+ fn serveWithDevice(
245
+ allocator: std.mem.Allocator,
246
+ device: anytype,
247
+ transport: []const u8,
248
+ port: u16,
249
+ trace_dir: ?[]const u8,
250
+ app_id: []const u8,
251
+ capture: trace.CaptureOptions,
252
+ ) !void {
253
+ var trace_writer: ?trace.TraceWriter = null;
254
+ if (trace_dir) |dir| {
255
+ trace_writer = try trace.TraceWriter.initWithOptions(allocator, dir, capture);
256
+ try trace_writer.?.startManifest("json-rpc session", app_id);
257
+ }
258
+ defer if (trace_writer) |*tw| tw.deinit();
259
+ const live_trace = if (trace_writer) |*tw| tw else null;
260
+
261
+ if (std.mem.eql(u8, transport, "stdio")) {
262
+ try json_rpc.serveStdioWithTrace(allocator, device, live_trace);
263
+ } else if (std.mem.eql(u8, transport, "tcp")) {
264
+ try json_rpc.serveTcpWithTrace(allocator, device, port, live_trace);
265
+ } else {
266
+ return error.UnsupportedTransport;
267
+ }
268
+ }
269
+
270
+ fn parsePlatform(value: []const u8) !run_options.Platform {
271
+ if (std.mem.eql(u8, value, "android")) return .android;
272
+ if (std.mem.eql(u8, value, "ios")) return .ios;
273
+ return error.UnsupportedPlatform;
274
+ }
275
+
276
+ fn parseIosDeviceType(value: []const u8) !run_options.IosDeviceType {
277
+ if (std.mem.eql(u8, value, "simulator")) return .simulator;
278
+ if (std.mem.eql(u8, value, "physical")) return .physical;
279
+ return error.UnsupportedIosDeviceType;
280
+ }
281
+
282
+ fn iosTargetKind(value: run_options.IosDeviceType) ios.TargetKind {
283
+ return switch (value) {
284
+ .simulator => .simulator,
285
+ .physical => .physical,
286
+ };
287
+ }
@@ -0,0 +1,111 @@
1
+ const std = @import("std");
2
+
3
+ const bundle = @import("bundle.zig");
4
+ const report = @import("report.zig");
5
+
6
+ pub const ReportArgs = struct {
7
+ input_path: []const u8,
8
+ out_path: ?[]const u8 = null,
9
+ };
10
+
11
+ pub const ExplainArgs = struct {
12
+ trace_dir: ?[]const u8 = null,
13
+ json: bool = false,
14
+ };
15
+
16
+ pub const ExportArgs = struct {
17
+ trace_dir: []const u8,
18
+ out_path: ?[]const u8 = null,
19
+ redact: bool = false,
20
+ omit_screenshots: bool = false,
21
+ };
22
+
23
+ pub fn parseReportArgs(args: []const []const u8) !ReportArgs {
24
+ if (args.len == 0) return error.MissingReportInput;
25
+ var parsed = ReportArgs{ .input_path = args[0] };
26
+
27
+ var index: usize = 1;
28
+ while (index < args.len) : (index += 1) {
29
+ const arg = args[index];
30
+ if (std.mem.eql(u8, arg, "--out")) {
31
+ index += 1;
32
+ parsed.out_path = if (index < args.len) args[index] else return error.MissingReportOutput;
33
+ } else {
34
+ return error.UnknownFlag;
35
+ }
36
+ }
37
+ if (parsed.out_path == null) return error.MissingReportOutput;
38
+ return parsed;
39
+ }
40
+
41
+ pub fn parseExplainArgs(args: []const []const u8) !ExplainArgs {
42
+ var parsed = ExplainArgs{};
43
+ for (args) |arg| {
44
+ if (std.mem.eql(u8, arg, "--json")) {
45
+ parsed.json = true;
46
+ } else if (parsed.trace_dir == null) {
47
+ parsed.trace_dir = arg;
48
+ } else {
49
+ return error.UnknownFlag;
50
+ }
51
+ }
52
+ if (parsed.trace_dir == null) return error.MissingTraceDir;
53
+ return parsed;
54
+ }
55
+
56
+ pub fn parseExportArgs(args: []const []const u8) !ExportArgs {
57
+ if (args.len == 0) return error.MissingTraceDir;
58
+ var parsed = ExportArgs{ .trace_dir = args[0] };
59
+
60
+ var index: usize = 1;
61
+ while (index < args.len) : (index += 1) {
62
+ const arg = args[index];
63
+ if (std.mem.eql(u8, arg, "--out")) {
64
+ index += 1;
65
+ parsed.out_path = if (index < args.len) args[index] else return error.MissingTraceBundleOutput;
66
+ } else if (std.mem.eql(u8, arg, "--redact")) {
67
+ parsed.redact = true;
68
+ } else if (std.mem.eql(u8, arg, "--omit-screenshots")) {
69
+ parsed.redact = true;
70
+ parsed.omit_screenshots = true;
71
+ } else {
72
+ return error.UnknownFlag;
73
+ }
74
+ }
75
+ if (parsed.out_path == null) return error.MissingTraceBundleOutput;
76
+ return parsed;
77
+ }
78
+
79
+ pub fn runReport(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
80
+ var raw_args = std.ArrayList([]const u8).empty;
81
+ defer raw_args.deinit(allocator);
82
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
83
+
84
+ const parsed = try parseReportArgs(raw_args.items);
85
+ try report.writeHtmlReport(allocator, parsed.input_path, parsed.out_path.?);
86
+ try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{parsed.out_path.?});
87
+ }
88
+
89
+ pub fn runExplain(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
90
+ var raw_args = std.ArrayList([]const u8).empty;
91
+ defer raw_args.deinit(allocator);
92
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
93
+
94
+ const parsed = try parseExplainArgs(raw_args.items);
95
+ const stdout = std.fs.File.stdout().deprecatedWriter();
96
+ if (parsed.json) return try report.writeTraceExplanationJson(allocator, parsed.trace_dir.?, stdout);
97
+ try report.writeTraceExplanation(allocator, parsed.trace_dir.?, stdout);
98
+ }
99
+
100
+ pub fn runExport(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
101
+ var raw_args = std.ArrayList([]const u8).empty;
102
+ defer raw_args.deinit(allocator);
103
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
104
+
105
+ const parsed = try parseExportArgs(raw_args.items);
106
+ try bundle.exportTraceBundleWithOptions(allocator, parsed.trace_dir, parsed.out_path.?, .{
107
+ .redact = parsed.redact,
108
+ .omit_screenshots = parsed.omit_screenshots,
109
+ });
110
+ try std.fs.File.stdout().deprecatedWriter().print("wrote {s}\n", .{parsed.out_path.?});
111
+ }