@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.
- package/CHANGELOG.md +6 -0
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/surface.js +9 -0
- package/dist/adapters/mobile/surface.js +9 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/surface.js +4 -1
- package/dist/commands/launch/device.js +346 -0
- package/dist/commands/launch/extension.js +303 -3
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/mobile.js +190 -2
- package/dist/commands/review.js +3 -1
- package/dist/commands/shared.js +2 -2
- package/docs/CONTRIBUTING.md +8 -3
- package/package.json +1 -1
|
@@ -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
|
};
|
package/dist/commands/review.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/commands/shared.js
CHANGED
|
@@ -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 &&
|
|
66
|
+
if (explicit && adapterRegistry.has(explicit)) return explicit;
|
|
67
67
|
if (hint) return hint;
|
|
68
68
|
return detectAdapter(target);
|
|
69
69
|
}
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -61,9 +61,14 @@ exits.
|
|
|
61
61
|
## Adapter surface
|
|
62
62
|
|
|
63
63
|
Commands resolve shared platform behavior through
|
|
64
|
-
`src/adapters/surface.ts`.
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|