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,212 @@
1
+ const std = @import("std");
2
+
3
+ pub fn errorFieldPathForFile(allocator: std.mem.Allocator, path: []const u8, err: anyerror) !?[]const u8 {
4
+ const content = std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024) catch return null;
5
+ defer allocator.free(content);
6
+ return try errorFieldPathForSlice(allocator, content, err);
7
+ }
8
+
9
+ pub fn errorFieldPathForSlice(allocator: std.mem.Allocator, content: []const u8, err: anyerror) !?[]const u8 {
10
+ if (err == error.ConfigMustBeObject) return try allocator.dupe(u8, "$");
11
+ const parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch return null;
12
+ defer parsed.deinit();
13
+ if (parsed.value != .object) return null;
14
+ const object = parsed.value.object;
15
+
16
+ return switch (err) {
17
+ error.MissingConfigSchemaVersion,
18
+ error.ConfigSchemaVersionMustBeInteger,
19
+ error.UnsupportedConfigVersion,
20
+ => try allocator.dupe(u8, "$.schemaVersion"),
21
+ error.ConfigUnknownField => try findUnknownFieldPath(allocator, object),
22
+ error.ConfigScriptsMustBeObject => try objectTypeFieldPath(allocator, object, "scripts", "$.scripts"),
23
+ error.ConfigPlatformMustBeObject => try firstObjectTypeFieldPath(allocator, object, &.{ "android", "ios" }),
24
+ error.ConfigToolsMustBeObject => try objectTypeFieldPath(allocator, object, "tools", "$.tools"),
25
+ error.ConfigArtifactsMustBeObject => try objectTypeFieldPath(allocator, object, "artifacts", "$.artifacts"),
26
+ error.ConfigRedactionMustBeObject => try objectTypeFieldPath(allocator, object, "redaction", "$.redaction"),
27
+ error.ConfigFieldMustBeBool => try findBoolFieldPath(allocator, object),
28
+ error.ConfigFieldMustBeString => try findStringFieldPath(allocator, object),
29
+ error.ConfigFieldMustBeNonEmptyString => try findNonEmptyStringFieldPath(allocator, object),
30
+ error.ConfigFieldMustBeStringArray => try findStringArrayFieldPath(allocator, object),
31
+ else => null,
32
+ };
33
+ }
34
+
35
+ fn findUnknownFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap) !?[]const u8 {
36
+ const root_allowed = [_][]const u8{ "schemaVersion", "appId", "android", "ios", "artifacts", "redaction", "tools", "scripts" };
37
+ if (try unknownInObject(allocator, object, "$", root_allowed[0..])) |path| return path;
38
+
39
+ const platform_allowed = [_][]const u8{
40
+ "enabled",
41
+ "defaultDevice",
42
+ "smokeScenario",
43
+ "traceDir",
44
+ "avdName",
45
+ "restoreSnapshot",
46
+ "createAvdIfMissing",
47
+ "avdSystemImage",
48
+ "avdDeviceProfile",
49
+ "resetBeforeRun",
50
+ "waitReady",
51
+ };
52
+ if (try unknownInNestedObject(allocator, object, "android", "$.android", platform_allowed[0..])) |path| return path;
53
+ if (try unknownInNestedObject(allocator, object, "ios", "$.ios", platform_allowed[0..])) |path| return path;
54
+
55
+ const tools_allowed = [_][]const u8{ "adbPath", "emulatorPath", "avdmanagerPath", "androidShimPath", "xcrunPath", "iosShimPath", "zigPath" };
56
+ if (try unknownInNestedObject(allocator, object, "tools", "$.tools", tools_allowed[0..])) |path| return path;
57
+
58
+ const artifact_allowed = [_][]const u8{ "screenshots", "hierarchy", "logs", "screenRecording" };
59
+ if (try unknownInNestedObject(allocator, object, "artifacts", "$.artifacts", artifact_allowed[0..])) |path| return path;
60
+
61
+ const redaction_allowed = [_][]const u8{ "denylistText", "allowlistText", "denylistResourceIds", "allowlistResourceIds" };
62
+ if (try unknownInNestedObject(allocator, object, "redaction", "$.redaction", redaction_allowed[0..])) |path| return path;
63
+
64
+ return null;
65
+ }
66
+
67
+ fn unknownInNestedObject(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8, prefix: []const u8, allowed: []const []const u8) !?[]const u8 {
68
+ const value = object.get(key) orelse return null;
69
+ if (value != .object) return null;
70
+ return try unknownInObject(allocator, value.object, prefix, allowed);
71
+ }
72
+
73
+ fn unknownInObject(allocator: std.mem.Allocator, object: std.json.ObjectMap, prefix: []const u8, allowed: []const []const u8) !?[]const u8 {
74
+ var iterator = object.iterator();
75
+ while (iterator.next()) |entry| {
76
+ if (!containsString(allowed, entry.key_ptr.*)) {
77
+ return try std.fmt.allocPrint(allocator, "{s}.{s}", .{ prefix, entry.key_ptr.* });
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+
83
+ fn containsString(values: []const []const u8, needle: []const u8) bool {
84
+ for (values) |value| {
85
+ if (std.mem.eql(u8, value, needle)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ fn objectTypeFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8, path: []const u8) !?[]const u8 {
91
+ const value = object.get(key) orelse return null;
92
+ if (value != .object) return try allocator.dupe(u8, path);
93
+ return null;
94
+ }
95
+
96
+ fn firstObjectTypeFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap, keys: []const []const u8) !?[]const u8 {
97
+ for (keys) |key| {
98
+ const value = object.get(key) orelse continue;
99
+ if (value != .object) return try std.fmt.allocPrint(allocator, "$.{s}", .{key});
100
+ }
101
+ return null;
102
+ }
103
+
104
+ fn findBoolFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap) !?[]const u8 {
105
+ const platform_bool_keys = [_][]const u8{ "enabled", "resetBeforeRun", "waitReady", "createAvdIfMissing" };
106
+ if (try fieldWithUnexpectedTag(allocator, object, "android", "$.android", platform_bool_keys[0..], .bool)) |path| return path;
107
+ if (try fieldWithUnexpectedTag(allocator, object, "ios", "$.ios", platform_bool_keys[0..], .bool)) |path| return path;
108
+
109
+ const artifact_bool_keys = [_][]const u8{ "screenshots", "hierarchy", "logs", "screenRecording" };
110
+ if (try fieldWithUnexpectedTag(allocator, object, "artifacts", "$.artifacts", artifact_bool_keys[0..], .bool)) |path| return path;
111
+ return null;
112
+ }
113
+
114
+ fn findStringFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap) !?[]const u8 {
115
+ if (try scriptValueWithUnexpectedTag(allocator, object, .string, false)) |path| return path;
116
+ if (try directFieldWithUnexpectedTag(allocator, object, "appId", "$.appId", .string, false)) |path| return path;
117
+ const platform_string_keys = [_][]const u8{ "defaultDevice", "smokeScenario", "traceDir", "avdName", "restoreSnapshot", "avdSystemImage", "avdDeviceProfile" };
118
+ if (try fieldWithUnexpectedTag(allocator, object, "android", "$.android", platform_string_keys[0..], .string)) |path| return path;
119
+ if (try fieldWithUnexpectedTag(allocator, object, "ios", "$.ios", platform_string_keys[0..], .string)) |path| return path;
120
+ const tools_string_keys = [_][]const u8{ "adbPath", "emulatorPath", "avdmanagerPath", "androidShimPath", "xcrunPath", "iosShimPath", "zigPath" };
121
+ if (try fieldWithUnexpectedTag(allocator, object, "tools", "$.tools", tools_string_keys[0..], .string)) |path| return path;
122
+ return null;
123
+ }
124
+
125
+ fn findNonEmptyStringFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap) !?[]const u8 {
126
+ if (try scriptValueWithUnexpectedTag(allocator, object, .string, true)) |path| return path;
127
+ if (try emptyDirectStringFieldPath(allocator, object, "appId", "$.appId")) |path| return path;
128
+ const platform_string_keys = [_][]const u8{ "defaultDevice", "smokeScenario", "traceDir", "avdName", "restoreSnapshot", "avdSystemImage", "avdDeviceProfile" };
129
+ if (try emptyNestedStringFieldPath(allocator, object, "android", "$.android", platform_string_keys[0..])) |path| return path;
130
+ if (try emptyNestedStringFieldPath(allocator, object, "ios", "$.ios", platform_string_keys[0..])) |path| return path;
131
+ const tools_string_keys = [_][]const u8{ "adbPath", "emulatorPath", "avdmanagerPath", "androidShimPath", "xcrunPath", "iosShimPath", "zigPath" };
132
+ if (try emptyNestedStringFieldPath(allocator, object, "tools", "$.tools", tools_string_keys[0..])) |path| return path;
133
+ const redaction_keys = [_][]const u8{ "denylistText", "allowlistText", "denylistResourceIds", "allowlistResourceIds" };
134
+ if (try emptyStringArrayItemPath(allocator, object, "redaction", "$.redaction", redaction_keys[0..])) |path| return path;
135
+ return null;
136
+ }
137
+
138
+ fn findStringArrayFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap) !?[]const u8 {
139
+ const redaction_keys = [_][]const u8{ "denylistText", "allowlistText", "denylistResourceIds", "allowlistResourceIds" };
140
+ const redaction = object.get("redaction") orelse return null;
141
+ if (redaction != .object) return null;
142
+ for (redaction_keys) |key| {
143
+ const value = redaction.object.get(key) orelse continue;
144
+ if (value != .array) return try std.fmt.allocPrint(allocator, "$.redaction.{s}", .{key});
145
+ for (value.array.items, 0..) |item, index| {
146
+ if (item != .string) return try std.fmt.allocPrint(allocator, "$.redaction.{s}[{d}]", .{ key, index });
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ fn fieldWithUnexpectedTag(allocator: std.mem.Allocator, object: std.json.ObjectMap, parent_key: []const u8, prefix: []const u8, keys: []const []const u8, expected: std.meta.Tag(std.json.Value)) !?[]const u8 {
153
+ const parent = object.get(parent_key) orelse return null;
154
+ if (parent != .object) return null;
155
+ for (keys) |key| {
156
+ if (try directFieldWithUnexpectedTag(allocator, parent.object, key, try std.fmt.allocPrint(allocator, "{s}.{s}", .{ prefix, key }), expected, true)) |path| return path;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ fn directFieldWithUnexpectedTag(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8, path: []const u8, expected: std.meta.Tag(std.json.Value), free_path: bool) !?[]const u8 {
162
+ defer if (free_path) allocator.free(path);
163
+ const value = object.get(key) orelse return null;
164
+ if (value == .null and expected == .string) return null;
165
+ if (std.meta.activeTag(value) != expected) return try allocator.dupe(u8, path);
166
+ return null;
167
+ }
168
+
169
+ fn scriptValueWithUnexpectedTag(allocator: std.mem.Allocator, object: std.json.ObjectMap, expected: std.meta.Tag(std.json.Value), reject_empty: bool) !?[]const u8 {
170
+ const scripts = object.get("scripts") orelse return null;
171
+ if (scripts != .object) return null;
172
+ var iterator = scripts.object.iterator();
173
+ while (iterator.next()) |entry| {
174
+ const path = try std.fmt.allocPrint(allocator, "$.scripts.{s}", .{entry.key_ptr.*});
175
+ errdefer allocator.free(path);
176
+ if (std.meta.activeTag(entry.value_ptr.*) != expected) return path;
177
+ if (reject_empty and entry.value_ptr.string.len == 0) return path;
178
+ allocator.free(path);
179
+ }
180
+ return null;
181
+ }
182
+
183
+ fn emptyDirectStringFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8, path: []const u8) !?[]const u8 {
184
+ const value = object.get(key) orelse return null;
185
+ if (value == .string and value.string.len == 0) return try allocator.dupe(u8, path);
186
+ return null;
187
+ }
188
+
189
+ fn emptyNestedStringFieldPath(allocator: std.mem.Allocator, object: std.json.ObjectMap, parent_key: []const u8, prefix: []const u8, keys: []const []const u8) !?[]const u8 {
190
+ const parent = object.get(parent_key) orelse return null;
191
+ if (parent != .object) return null;
192
+ for (keys) |key| {
193
+ const value = parent.object.get(key) orelse continue;
194
+ const path = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ prefix, key });
195
+ defer allocator.free(path);
196
+ if (value == .string and value.string.len == 0) return try allocator.dupe(u8, path);
197
+ }
198
+ return null;
199
+ }
200
+
201
+ fn emptyStringArrayItemPath(allocator: std.mem.Allocator, object: std.json.ObjectMap, parent_key: []const u8, prefix: []const u8, keys: []const []const u8) !?[]const u8 {
202
+ const parent = object.get(parent_key) orelse return null;
203
+ if (parent != .object) return null;
204
+ for (keys) |key| {
205
+ const value = parent.object.get(key) orelse continue;
206
+ if (value != .array) continue;
207
+ for (value.array.items, 0..) |item, index| {
208
+ if (item == .string and item.string.len == 0) return try std.fmt.allocPrint(allocator, "{s}.{s}[{d}]", .{ prefix, key, index });
209
+ }
210
+ }
211
+ return null;
212
+ }
@@ -0,0 +1,49 @@
1
+ const std = @import("std");
2
+ const config = @import("config.zig");
3
+
4
+ pub const default_path = ".zmr/config.json";
5
+
6
+ pub fn loadIfPresent(allocator: std.mem.Allocator, explicit_path: ?[]const u8) !?config.Config {
7
+ if (explicit_path) |path| return try config.parseFile(allocator, path);
8
+ std.fs.cwd().access(default_path, .{}) catch |err| switch (err) {
9
+ error.FileNotFound => return null,
10
+ else => return err,
11
+ };
12
+ return try config.parseFile(allocator, default_path);
13
+ }
14
+
15
+ pub fn rootForPath(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
16
+ const parent = std.fs.path.dirname(path) orelse ".";
17
+ if (std.mem.eql(u8, std.fs.path.basename(parent), ".zmr")) {
18
+ return try allocator.dupe(u8, std.fs.path.dirname(parent) orelse ".");
19
+ }
20
+ return try allocator.dupe(u8, ".");
21
+ }
22
+
23
+ pub fn ownFilePath(
24
+ allocator: std.mem.Allocator,
25
+ owned_paths: *std.ArrayList([]const u8),
26
+ config_root: []const u8,
27
+ path: []const u8,
28
+ ) ![]const u8 {
29
+ const resolved = if (std.fs.path.isAbsolute(path))
30
+ try allocator.dupe(u8, path)
31
+ else
32
+ try std.fs.path.join(allocator, &.{ config_root, path });
33
+ try owned_paths.append(allocator, resolved);
34
+ return resolved;
35
+ }
36
+
37
+ pub fn ownCommandPath(
38
+ allocator: std.mem.Allocator,
39
+ owned_paths: *std.ArrayList([]const u8),
40
+ config_root: []const u8,
41
+ path: []const u8,
42
+ ) ![]const u8 {
43
+ if (!std.fs.path.isAbsolute(path) and std.mem.indexOfScalar(u8, path, std.fs.path.sep) == null and !std.mem.startsWith(u8, path, ".")) {
44
+ const owned = try allocator.dupe(u8, path);
45
+ try owned_paths.append(allocator, owned);
46
+ return owned;
47
+ }
48
+ return try ownFilePath(allocator, owned_paths, config_root, path);
49
+ }
@@ -0,0 +1,37 @@
1
+ const std = @import("std");
2
+ const trace = @import("trace.zig");
3
+ const types = @import("types.zig");
4
+
5
+ pub const Platform = enum {
6
+ android,
7
+ ios,
8
+ };
9
+
10
+ pub fn isReady(platform: Platform, state: []const u8) bool {
11
+ return switch (platform) {
12
+ .android => std.mem.eql(u8, state, "device"),
13
+ .ios => std.mem.eql(u8, state, "Booted") or
14
+ std.mem.eql(u8, state, "connected") or
15
+ std.mem.eql(u8, state, "available"),
16
+ };
17
+ }
18
+
19
+ pub fn isKnownReadyState(state: []const u8) bool {
20
+ return isReady(.android, state) or isReady(.ios, state);
21
+ }
22
+
23
+ pub fn writeJson(writer: anytype, platform: Platform, devices: []const types.DeviceInfo) !void {
24
+ try writer.writeAll("{\"platform\":");
25
+ try trace.writeJsonString(writer, @tagName(platform));
26
+ try writer.print(",\"count\":{d},\"devices\":[", .{devices.len});
27
+ for (devices, 0..) |device, index| {
28
+ if (index > 0) try writer.writeAll(",");
29
+ try writer.writeAll("{\"serial\":");
30
+ try trace.writeJsonString(writer, device.serial);
31
+ try writer.writeAll(",\"state\":");
32
+ try trace.writeJsonString(writer, device.state);
33
+ try writer.print(",\"ready\":{}", .{isReady(platform, device.state)});
34
+ try writer.writeAll("}");
35
+ }
36
+ try writer.writeAll("]}\n");
37
+ }