@deeeed/metamask-harness 0.73.0 → 0.74.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.
@@ -1,3 +1,4 @@
1
+ import fs from "node:fs";
1
2
  import { prepareMobile } from "../../adapters/mobile/prepare.js";
2
3
  import {
3
4
  mobileSourceCheck,
@@ -7,7 +8,11 @@ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
7
8
  import { pinMobileFeatureFlags } from "../../adapters/mobile/feature-flags.js";
8
9
  import { toRemoteFlagSpec } from "../../feature-flags.js";
9
10
  import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
10
- import { EXIT } from "../shared.js";
11
+ import { EXIT, str, writeInteractiveProgress } from "../shared.js";
12
+ import { applyDeviceTargeting } from "../device-target.js";
13
+ import { RECOVERY_CODE } from "../../heal-bounds.js";
14
+ import { recipeRuntimePath } from "../../paths.js";
15
+ import { shellQuote } from "./device.js";
11
16
  async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false, remoteFlags) {
12
17
  clearMobileReleaseArtifactState(target);
13
18
  await ensureHarnessFresh(target, "mobile");
@@ -54,6 +59,189 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
54
59
  }
55
60
  return { ...result, remoteFeatureFlags: provenFlags };
56
61
  }
62
+ function mobileProvisioningBlocked(output) {
63
+ return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
64
+ }
65
+ function missingConfiguredIosSimulator(output) {
66
+ return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
67
+ }
68
+ function ambiguousConfiguredIosSimulator(output) {
69
+ return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
70
+ }
71
+ function mobileBridgeTargetMissing(output) {
72
+ return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
73
+ }
74
+ function mobileProvisionCommand(target, mobileTarget) {
75
+ const platform = mobileTarget === "android" ? "android" : "ios";
76
+ const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
77
+ const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
78
+ if (device) parts.push("--device", shellQuote(device));
79
+ return parts.join(" ");
80
+ }
81
+ function isIosSimulatorUdid(value) {
82
+ return /^[0-9A-Fa-f-]{36}$/u.test(value);
83
+ }
84
+ function launchDeviceLabel(mobileTarget) {
85
+ if (mobileTarget === "android") {
86
+ return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
87
+ }
88
+ if (mobileTarget === "ios") {
89
+ return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
90
+ }
91
+ return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
92
+ }
93
+ function metroLaunchEvidence(target) {
94
+ const evidencePath = recipeRuntimePath(target, "metro-generation.json");
95
+ let descriptor;
96
+ try {
97
+ descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
98
+ const stat = fs.fstatSync(descriptor);
99
+ if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
100
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
101
+ } catch {
102
+ return void 0;
103
+ } finally {
104
+ if (descriptor !== void 0) fs.closeSync(descriptor);
105
+ }
106
+ }
107
+ const mobileLaunchProfile = {
108
+ refuseFlags({ mobileTarget }) {
109
+ if (!mobileTarget) {
110
+ return { message: "target is required for mobile.", userAction: "mm-harness launch ios or mm-harness launch android" };
111
+ }
112
+ return void 0;
113
+ },
114
+ applyEnv({ adapter, options, mobileTarget, target }) {
115
+ const device = str(options, "device");
116
+ if (!device && mobileTarget !== "ios" && mobileTarget !== "android") return void 0;
117
+ const targetingOptions = {
118
+ ...options,
119
+ ...mobileTarget ? { platform: mobileTarget } : {}
120
+ };
121
+ const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
122
+ if ("code" in result) {
123
+ if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
124
+ process.env.IOS_SIMULATOR = device;
125
+ process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
126
+ process.env.ADB_SERIAL = "";
127
+ process.env.ANDROID_SERIAL = "";
128
+ process.env.ANDROID_DEVICE = "";
129
+ process.env.ANDROID_TARGET_DEVICE_NAME = "";
130
+ } else {
131
+ return result;
132
+ }
133
+ }
134
+ if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
135
+ return {
136
+ ok: false,
137
+ code: "DEVICE_WRONG_PLATFORM",
138
+ message: `--device ${device} did not resolve to an Android device for launch android.`,
139
+ userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
140
+ };
141
+ }
142
+ if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
143
+ return {
144
+ ok: false,
145
+ code: "DEVICE_WRONG_PLATFORM",
146
+ message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
147
+ userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
148
+ };
149
+ }
150
+ return void 0;
151
+ },
152
+ start({ options, mobileTarget, machine, tier, wantClearMetro }) {
153
+ const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
154
+ const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
155
+ writeInteractiveProgress(
156
+ machine,
157
+ `\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
158
+ );
159
+ },
160
+ nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
161
+ rebuildCommand: ({ mobileTarget }) => `mm-harness launch ${mobileTarget} --build`,
162
+ compose: ({ target, mobileTarget, machine, remoteFlagRequest }, tier, restartApp, clearMetro) => launchMobile(target, mobileTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags),
163
+ classify(_launch, output) {
164
+ if (output.includes("open-device: Android device is locked.")) {
165
+ return {
166
+ code: "MOBILE_DEVICE_LOCKED",
167
+ message: "the selected Android device is locked.",
168
+ recoverable: false,
169
+ userAction: "Unlock the selected Android device, then retry the launch.",
170
+ exitCode: EXIT.runtime,
171
+ originalError: output.trim()
172
+ };
173
+ }
174
+ if (/open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(output)) {
175
+ return {
176
+ code: "MOBILE_DEVICE_UI_UNAVAILABLE",
177
+ message: "the selected Android display could not be prepared.",
178
+ recoverable: false,
179
+ userAction: "Check adb access and the selected device screen, then retry the launch.",
180
+ exitCode: EXIT.runtime,
181
+ originalError: output.trim()
182
+ };
183
+ }
184
+ return void 0;
185
+ },
186
+ async beforeHeal(launch, attempt, state) {
187
+ const { target, mobileTarget, tier } = launch;
188
+ const output = attempt.output;
189
+ const missingIosSimulator = missingConfiguredIosSimulator(output);
190
+ const ambiguousIosSimulator = ambiguousConfiguredIosSimulator(output);
191
+ if (ambiguousIosSimulator) {
192
+ return launch.fail(state, {
193
+ code: "MOBILE_DEVICE_AMBIGUOUS",
194
+ message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
195
+ recoverable: false,
196
+ userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
197
+ exitCode: EXIT.runtime,
198
+ originalError: output.trim() || void 0
199
+ });
200
+ }
201
+ if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
202
+ return launch.fail(state, {
203
+ code: "MOBILE_DEVICE_NOT_FOUND",
204
+ message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
205
+ recoverable: false,
206
+ userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
207
+ exitCode: EXIT.runtime,
208
+ originalError: output.trim() || void 0
209
+ });
210
+ }
211
+ if (mobileProvisioningBlocked(output)) {
212
+ return launch.fail(state, {
213
+ code: "MOBILE_PROVISION_REQUIRED",
214
+ message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
215
+ recoverable: false,
216
+ userAction: mobileProvisionCommand(target, mobileTarget),
217
+ exitCode: EXIT.runtime,
218
+ originalError: output.trim() || void 0
219
+ });
220
+ }
221
+ if (tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(output)) {
222
+ return launch.fail(state, {
223
+ code: "MOBILE_NATIVE_BUILD_FAILED",
224
+ message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
225
+ recoverable: false,
226
+ exitCode: EXIT.runtime,
227
+ originalError: output.trim() || void 0
228
+ });
229
+ }
230
+ return void 0;
231
+ },
232
+ recovery(_launch, output) {
233
+ const restartApp = mobileBridgeTargetMissing(output);
234
+ return { code: restartApp ? "mobile.app-restarted" : RECOVERY_CODE.mobile, restartApp };
235
+ },
236
+ // Mobile proved its set inside launchMobile (null: nothing requested and no
237
+ // runtime answered).
238
+ provenFlags: (_launch, requested, proven) => ({ flags: proven === void 0 ? requested : proven }),
239
+ verifyArgs: ({ mobileTarget }) => mobileTarget ? ["--platform", mobileTarget] : [],
240
+ platformLabel: ({ mobileTarget }) => mobileTarget ?? null,
241
+ deviceLabel: ({ mobileTarget }) => launchDeviceLabel(mobileTarget),
242
+ evidence: metroLaunchEvidence
243
+ };
57
244
  export {
58
- launchMobile
245
+ launchMobile,
246
+ mobileLaunchProfile
59
247
  };
@@ -8,6 +8,7 @@ import {
8
8
  parityAdapter,
9
9
  readConfig,
10
10
  referenceHint,
11
+ REVIEW_ADAPTERS,
11
12
  renderReviewChecklist,
12
13
  renderReviewHelp,
13
14
  resolveLibrary,
@@ -30,7 +31,8 @@ function adapterFromOptions(options, target) {
30
31
  if (raw !== "mobile" && raw !== "extension" && raw !== "core" && raw !== "terminal") throw usageError(`--adapter must be mobile, extension, core, or terminal (got ${raw}).`);
31
32
  return raw;
32
33
  }
33
- return detectAdapter(target) ?? "unscoped";
34
+ const detected = detectAdapter(target);
35
+ return REVIEW_ADAPTERS.find((adapter) => adapter === detected) ?? "unscoped";
34
36
  }
35
37
  function resolveReviewContext(options, { fetch = true } = {}) {
36
38
  const target = targetPath(options);
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { detectAdapter } from "../harness.js";
8
8
  import { colorHumanMessage } from "../cli-color.js";
9
9
  import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
10
+ import { adapterRegistry } from "../adapters/registry.js";
10
11
  const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
11
12
  function writeInteractiveProgress(json, message, {
12
13
  stdoutIsTTY = Boolean(process.stdout.isTTY),
@@ -59,11 +60,10 @@ function flag(options, key) {
59
60
  function targetOf(options) {
60
61
  return path.resolve(str(options, "target") ?? process.cwd());
61
62
  }
62
- const ADAPTER_TOKENS = ["mobile", "extension", "core", "terminal"];
63
63
  function resolveAdapter(options, target, hint) {
64
64
  const platform = str(options, "platform");
65
65
  const explicit = str(options, "adapter") ?? (platform === "ios" || platform === "android" ? "mobile" : platform);
66
- if (explicit && ADAPTER_TOKENS.includes(explicit)) return explicit;
66
+ if (explicit && adapterRegistry.has(explicit)) return explicit;
67
67
  if (hint) return hint;
68
68
  return detectAdapter(target);
69
69
  }
@@ -61,9 +61,14 @@ exits.
61
61
  ## Adapter surface
62
62
 
63
63
  Commands resolve shared platform behavior through
64
- `src/adapters/surface.ts`. Extend `AdapterSurface` only when all platforms need
65
- the concept, implement all three adapters, and let Core report headless/N/A
66
- explicitly. Do not add command-local platform ladders for surface-owned behavior.
64
+ `src/adapters/surface.ts`. Each platform's surface registers itself in
65
+ `src/adapters/registry.ts` when its module loads; a new platform registers the
66
+ same way and needs no edit to shared code. Extend `AdapterSurface` only when all
67
+ platforms need the concept, implement it in every adapter, and let Core report
68
+ headless/N/A explicitly. Do not add command-local platform ladders for
69
+ surface-owned behavior: `launch` already dispatches through
70
+ `AdapterSurface.launch`, and the device platforms supply a `DeviceLaunchProfile`
71
+ (`src/commands/launch/device.ts`).
67
72
 
68
73
  Each checkout owns:
69
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"