@deeeed/metamask-harness 0.36.0 → 0.37.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 +24 -0
- package/adapters/extension/artifact-runtime-state.cjs +128 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +22 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +9 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +161 -20
- package/dist/adapters/extension/runtime.js +127 -0
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +11 -4
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +2 -1
- package/dist/commands/launch/extension.js +55 -6
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +89 -5
- package/dist/commands/run.js +3 -1
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/run-diagnostics.js +1 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +150 -0
- package/library/actions/extension/perps/perps.mjs +2 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/platform/cdp.mjs +6 -3
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/mobile/platform/observe-ui.mjs +84 -2
- package/library/actions/mobile/ui/native-navigation.mjs +225 -0
- package/library/actions/mobile/ui/navigate.mjs +7 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +493 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +116 -0
- package/library/manifests/mobile.action-manifest.json +16 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { promises as fsp } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
async function installAndroidReleaseArtifact(options, deps = defaultDeps) {
|
|
8
|
+
validateInputs(options);
|
|
9
|
+
const sourcePath = path.resolve(options.file);
|
|
10
|
+
const stagingDirectory = await fsp.mkdtemp(path.join(os.tmpdir(), "mm-harness-android-artifact-"));
|
|
11
|
+
try {
|
|
12
|
+
const stagedPath = path.join(stagingDirectory, "release.apk");
|
|
13
|
+
const { sha256, bytes } = await stageRegularFile(sourcePath, stagedPath);
|
|
14
|
+
if (sha256 !== options.expectedSha256) {
|
|
15
|
+
throw new Error(`Android artifact SHA-256 ${sha256} does not match expected ${options.expectedSha256}.`);
|
|
16
|
+
}
|
|
17
|
+
const { packageId, versionName, versionCode } = readAndroidPackageIdentity(deps, stagedPath);
|
|
18
|
+
if (packageId !== options.packageId) {
|
|
19
|
+
throw new Error(`Android artifact package ${packageId || "missing"} does not match expected ${options.packageId}.`);
|
|
20
|
+
}
|
|
21
|
+
if (versionName !== options.expectedVersion) {
|
|
22
|
+
throw new Error(`Android artifact version ${versionName || "missing"} does not match expected ${options.expectedVersion}.`);
|
|
23
|
+
}
|
|
24
|
+
if (versionCode !== options.expectedBuildId) {
|
|
25
|
+
throw new Error(`Android artifact build ${versionCode || "missing"} does not match expected ${options.expectedBuildId}.`);
|
|
26
|
+
}
|
|
27
|
+
const adb = resolveAdb(deps);
|
|
28
|
+
const state = runChecked(deps, adb, ["-s", options.deviceSerial, "get-state"]).trim();
|
|
29
|
+
if (state !== "device") {
|
|
30
|
+
throw new Error(`Android device ${options.deviceSerial} is not ready.`);
|
|
31
|
+
}
|
|
32
|
+
runChecked(deps, adb, ["-s", options.deviceSerial, "install", "-r", "-d", stagedPath]);
|
|
33
|
+
assertInstalledAndroidReleaseArtifact({
|
|
34
|
+
packageId,
|
|
35
|
+
versionName,
|
|
36
|
+
versionCode,
|
|
37
|
+
sha256,
|
|
38
|
+
deviceSerial: options.deviceSerial
|
|
39
|
+
}, deps, adb);
|
|
40
|
+
runChecked(deps, adb, [
|
|
41
|
+
"-s",
|
|
42
|
+
options.deviceSerial,
|
|
43
|
+
"shell",
|
|
44
|
+
"monkey",
|
|
45
|
+
"-p",
|
|
46
|
+
packageId,
|
|
47
|
+
"-c",
|
|
48
|
+
"android.intent.category.LAUNCHER",
|
|
49
|
+
"1"
|
|
50
|
+
]);
|
|
51
|
+
let pid = "";
|
|
52
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
53
|
+
const result = deps.run(adb, ["-s", options.deviceSerial, "shell", "pidof", packageId]);
|
|
54
|
+
if (result.status === 0 && result.stdout.trim()) {
|
|
55
|
+
pid = result.stdout.trim();
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
await deps.sleep(250);
|
|
59
|
+
}
|
|
60
|
+
if (!pid) throw new Error(`Android package ${packageId} did not start on ${options.deviceSerial}.`);
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
status: "pass",
|
|
64
|
+
adapter: "mobile",
|
|
65
|
+
platform: "android",
|
|
66
|
+
source: { kind: "local", path: sourcePath },
|
|
67
|
+
sha256,
|
|
68
|
+
bytes,
|
|
69
|
+
packageId,
|
|
70
|
+
versionName,
|
|
71
|
+
versionCode,
|
|
72
|
+
deviceSerial: options.deviceSerial,
|
|
73
|
+
installed: true,
|
|
74
|
+
launched: true
|
|
75
|
+
};
|
|
76
|
+
} finally {
|
|
77
|
+
await fsp.rm(stagingDirectory, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function assertAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
|
|
81
|
+
validateInputs({
|
|
82
|
+
file: "runtime.apk",
|
|
83
|
+
expectedVersion: identity.versionName,
|
|
84
|
+
expectedBuildId: identity.versionCode,
|
|
85
|
+
expectedSha256: identity.sha256,
|
|
86
|
+
packageId: identity.packageId,
|
|
87
|
+
deviceSerial: identity.deviceSerial
|
|
88
|
+
});
|
|
89
|
+
const adb = resolveAdb(deps);
|
|
90
|
+
assertInstalledAndroidReleaseArtifact(identity, deps, adb);
|
|
91
|
+
}
|
|
92
|
+
async function relaunchAndroidReleaseArtifactRuntime(identity, deps = defaultDeps) {
|
|
93
|
+
assertAndroidReleaseArtifactRuntime(identity, deps);
|
|
94
|
+
const adb = resolveAdb(deps);
|
|
95
|
+
runChecked(deps, adb, [
|
|
96
|
+
"-s",
|
|
97
|
+
identity.deviceSerial,
|
|
98
|
+
"shell",
|
|
99
|
+
"monkey",
|
|
100
|
+
"-p",
|
|
101
|
+
identity.packageId,
|
|
102
|
+
"-c",
|
|
103
|
+
"android.intent.category.LAUNCHER",
|
|
104
|
+
"1"
|
|
105
|
+
]);
|
|
106
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
107
|
+
const result = deps.run(adb, ["-s", identity.deviceSerial, "shell", "pidof", identity.packageId]);
|
|
108
|
+
if (result.status === 0 && result.stdout.trim()) return;
|
|
109
|
+
await deps.sleep(250);
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`Android package ${identity.packageId} did not start on ${identity.deviceSerial}.`);
|
|
112
|
+
}
|
|
113
|
+
function validateInputs(options) {
|
|
114
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?$/u.test(options.expectedVersion)) {
|
|
115
|
+
throw new Error("--artifact-version must be an Android version name such as X.Y.Z.");
|
|
116
|
+
}
|
|
117
|
+
if (!/^\d+$/u.test(options.expectedBuildId)) {
|
|
118
|
+
throw new Error("--artifact-build-id must be the numeric Android version code.");
|
|
119
|
+
}
|
|
120
|
+
if (!/^[a-f0-9]{64}$/u.test(options.expectedSha256)) {
|
|
121
|
+
throw new Error("--artifact-sha256 must be 64 lowercase hexadecimal characters.");
|
|
122
|
+
}
|
|
123
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+$/u.test(options.packageId)) {
|
|
124
|
+
throw new Error("--artifact-package-id must be an Android application ID.");
|
|
125
|
+
}
|
|
126
|
+
if (!/^[A-Za-z0-9._:-]+$/u.test(options.deviceSerial)) {
|
|
127
|
+
throw new Error("Android artifact launch requires an explicit device serial.");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function assertInstalledAndroidReleaseArtifact(identity, deps, adb) {
|
|
131
|
+
const state = runChecked(deps, adb, ["-s", identity.deviceSerial, "get-state"]).trim();
|
|
132
|
+
if (state !== "device") throw new Error(`Android device ${identity.deviceSerial} is not ready.`);
|
|
133
|
+
const paths = runChecked(deps, adb, [
|
|
134
|
+
"-s",
|
|
135
|
+
identity.deviceSerial,
|
|
136
|
+
"shell",
|
|
137
|
+
"pm",
|
|
138
|
+
"path",
|
|
139
|
+
identity.packageId
|
|
140
|
+
]).split(/\r?\n/u).filter(Boolean);
|
|
141
|
+
if (paths.length !== 1 || !paths[0].startsWith("package:/") || !paths[0].endsWith("/base.apk")) {
|
|
142
|
+
throw new Error(`Android package ${identity.packageId} is not a single installed APK on ${identity.deviceSerial}.`);
|
|
143
|
+
}
|
|
144
|
+
const installedPath = paths[0].slice("package:".length);
|
|
145
|
+
if (!/^\/data\/app\/[A-Za-z0-9._~+=/-]+\/base\.apk$/u.test(installedPath)) {
|
|
146
|
+
throw new Error(`Android package ${identity.packageId} returned an unsafe installed path.`);
|
|
147
|
+
}
|
|
148
|
+
const installedSha256 = runChecked(deps, adb, [
|
|
149
|
+
"-s",
|
|
150
|
+
identity.deviceSerial,
|
|
151
|
+
"shell",
|
|
152
|
+
"sha256sum",
|
|
153
|
+
installedPath
|
|
154
|
+
]).trim().split(/\s+/u)[0];
|
|
155
|
+
if (installedSha256 !== identity.sha256) {
|
|
156
|
+
throw new Error(`Android package ${identity.packageId} no longer matches the trusted release artifact SHA-256.`);
|
|
157
|
+
}
|
|
158
|
+
const installed = runChecked(deps, adb, [
|
|
159
|
+
"-s",
|
|
160
|
+
identity.deviceSerial,
|
|
161
|
+
"shell",
|
|
162
|
+
"dumpsys",
|
|
163
|
+
"package",
|
|
164
|
+
identity.packageId
|
|
165
|
+
]);
|
|
166
|
+
if (!new RegExp(`^\\s*versionName=${escapeRegExp(identity.versionName)}\\s*$`, "mu").test(installed) || !new RegExp(`^\\s*versionCode=${escapeRegExp(identity.versionCode)}(?:\\s|$)`, "mu").test(installed)) {
|
|
167
|
+
throw new Error(`Android package ${identity.packageId} installed with an unexpected version on ${identity.deviceSerial}.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function escapeRegExp(value) {
|
|
171
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
172
|
+
}
|
|
173
|
+
async function stageRegularFile(file, destination) {
|
|
174
|
+
const descriptor = await fsp.open(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
175
|
+
const output = await fsp.open(destination, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 384);
|
|
176
|
+
try {
|
|
177
|
+
const stat = await descriptor.stat();
|
|
178
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > 2 * 1024 * 1024 * 1024) {
|
|
179
|
+
throw new Error(`Android artifact must be a non-empty regular file smaller than 2 GiB: ${file}`);
|
|
180
|
+
}
|
|
181
|
+
const digest = createHash("sha256");
|
|
182
|
+
const stream = descriptor.createReadStream({ autoClose: false });
|
|
183
|
+
for await (const chunk of stream) {
|
|
184
|
+
digest.update(chunk);
|
|
185
|
+
await output.writeFile(chunk);
|
|
186
|
+
}
|
|
187
|
+
await output.sync();
|
|
188
|
+
return { sha256: digest.digest("hex"), bytes: stat.size };
|
|
189
|
+
} finally {
|
|
190
|
+
await output.close();
|
|
191
|
+
await descriptor.close();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function resolveAdb(deps) {
|
|
195
|
+
const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
196
|
+
const candidates = [
|
|
197
|
+
configured ? path.join(configured, "platform-tools", "adb") : "",
|
|
198
|
+
path.join(os.homedir(), "Library", "Android", "sdk", "platform-tools", "adb"),
|
|
199
|
+
"adb"
|
|
200
|
+
].filter(Boolean);
|
|
201
|
+
return firstRunnable(deps, candidates, ["version"], "adb");
|
|
202
|
+
}
|
|
203
|
+
function readAndroidPackageIdentity(deps, file) {
|
|
204
|
+
const analyzer = resolveApkAnalyzer(deps);
|
|
205
|
+
if (analyzer) {
|
|
206
|
+
const packageId2 = deps.run(analyzer, ["manifest", "application-id", file]);
|
|
207
|
+
const versionName2 = deps.run(analyzer, ["manifest", "version-name", file]);
|
|
208
|
+
const versionCode2 = deps.run(analyzer, ["manifest", "version-code", file]);
|
|
209
|
+
if (packageId2.status === 0 && packageId2.stdout.trim() && versionName2.status === 0 && versionName2.stdout.trim() && versionCode2.status === 0 && versionCode2.stdout.trim()) {
|
|
210
|
+
return {
|
|
211
|
+
packageId: packageId2.stdout.trim(),
|
|
212
|
+
versionName: versionName2.stdout.trim(),
|
|
213
|
+
versionCode: versionCode2.stdout.trim()
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const aapt2 = resolveAapt2(deps);
|
|
218
|
+
const badging = runChecked(deps, aapt2, ["dump", "badging", file]);
|
|
219
|
+
const line = badging.split(/\r?\n/u).find((entry) => entry.startsWith("package: ")) ?? "";
|
|
220
|
+
const packageId = /\bname='([^']+)'/u.exec(line)?.[1] ?? "";
|
|
221
|
+
const versionCode = /\bversionCode='([^']+)'/u.exec(line)?.[1] ?? "";
|
|
222
|
+
const versionName = /\bversionName='([^']+)'/u.exec(line)?.[1] ?? "";
|
|
223
|
+
if (!packageId || !versionCode || !versionName) {
|
|
224
|
+
throw new Error("aapt2 could not read the Android artifact package identity.");
|
|
225
|
+
}
|
|
226
|
+
return { packageId, versionName, versionCode };
|
|
227
|
+
}
|
|
228
|
+
function resolveApkAnalyzer(deps) {
|
|
229
|
+
const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
230
|
+
const candidates = [
|
|
231
|
+
configured ? path.join(configured, "cmdline-tools", "latest", "bin", "apkanalyzer") : "",
|
|
232
|
+
path.join(os.homedir(), "Library", "Android", "sdk", "cmdline-tools", "latest", "bin", "apkanalyzer")
|
|
233
|
+
].filter(Boolean);
|
|
234
|
+
for (const candidate of candidates) {
|
|
235
|
+
try {
|
|
236
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
237
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
238
|
+
} catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const probe = deps.run("apkanalyzer", []);
|
|
243
|
+
if (`${probe.stdout}
|
|
244
|
+
${probe.stderr}`.includes("manifest application-id")) return "apkanalyzer";
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function resolveAapt2(deps) {
|
|
248
|
+
const configured = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT;
|
|
249
|
+
const buildToolsRoot = configured ? path.join(configured, "build-tools") : path.join(os.homedir(), "Library", "Android", "sdk", "build-tools");
|
|
250
|
+
let candidates = [];
|
|
251
|
+
try {
|
|
252
|
+
candidates = fs.readdirSync(buildToolsRoot).sort((left, right) => right.localeCompare(left, void 0, { numeric: true })).map((version) => path.join(buildToolsRoot, version, "aapt2"));
|
|
253
|
+
} catch {
|
|
254
|
+
candidates = [];
|
|
255
|
+
}
|
|
256
|
+
candidates.push("aapt2");
|
|
257
|
+
return firstRunnable(deps, candidates, ["version"], "apkanalyzer or aapt2");
|
|
258
|
+
}
|
|
259
|
+
function firstRunnable(deps, candidates, args, label) {
|
|
260
|
+
for (const candidate of candidates) {
|
|
261
|
+
if (deps.run(candidate, args).status === 0) return candidate;
|
|
262
|
+
}
|
|
263
|
+
throw new Error(`${label} is required to validate and install an Android release artifact.`);
|
|
264
|
+
}
|
|
265
|
+
function runChecked(deps, command, args) {
|
|
266
|
+
const result = deps.run(command, args);
|
|
267
|
+
if (result.status !== 0) {
|
|
268
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`;
|
|
269
|
+
throw new Error(`${path.basename(command)} failed: ${detail}`);
|
|
270
|
+
}
|
|
271
|
+
return result.stdout;
|
|
272
|
+
}
|
|
273
|
+
const defaultDeps = {
|
|
274
|
+
run(command, args) {
|
|
275
|
+
const result = spawnSync(command, args, {
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
278
|
+
timeout: 12e4,
|
|
279
|
+
maxBuffer: 4 * 1024 * 1024
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
status: result.status ?? 1,
|
|
283
|
+
stdout: result.stdout || "",
|
|
284
|
+
stderr: result.stderr || result.error?.message || ""
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
sleep(ms) {
|
|
288
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
export {
|
|
292
|
+
assertAndroidReleaseArtifactRuntime,
|
|
293
|
+
installAndroidReleaseArtifact,
|
|
294
|
+
relaunchAndroidReleaseArtifactRuntime
|
|
295
|
+
};
|
package/dist/adapters.js
CHANGED
|
@@ -12,10 +12,15 @@ import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-p
|
|
|
12
12
|
import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
14
|
const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
|
|
15
|
+
"ui.press",
|
|
16
|
+
"ui.set_input",
|
|
17
|
+
"ui.scroll",
|
|
15
18
|
"ui.swipe",
|
|
16
19
|
"ui.pan",
|
|
17
20
|
"ui.drag",
|
|
18
21
|
"ui.long_press",
|
|
22
|
+
"ui.wait_for",
|
|
23
|
+
"ui.screenshot",
|
|
19
24
|
"ui.capture_surface"
|
|
20
25
|
]);
|
|
21
26
|
function sleep(ms) {
|
|
@@ -30,6 +35,7 @@ function simpleAdapter(action, executor) {
|
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
|
|
38
|
+
"metamask.perps.read_snapshot",
|
|
33
39
|
"metamask.perps.read_positions",
|
|
34
40
|
"metamask.perps.ensure_positions",
|
|
35
41
|
"metamask.perps.assert_positions",
|
|
@@ -47,8 +53,7 @@ const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
47
53
|
"metamask.perps.capture_performance"
|
|
48
54
|
]);
|
|
49
55
|
const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
|
|
50
|
-
"metamask.perps.read_account"
|
|
51
|
-
"metamask.perps.read_snapshot"
|
|
56
|
+
"metamask.perps.read_account"
|
|
52
57
|
]);
|
|
53
58
|
const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
54
59
|
"metamask.wallet.setup",
|
|
@@ -221,13 +226,14 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
|
|
|
221
226
|
"metamask.perps.start_state",
|
|
222
227
|
"metamask.perps.teardown_state",
|
|
223
228
|
"metamask.perps.capture_performance",
|
|
229
|
+
...platform === "extension" || platform === "core" ? ["metamask.perps.read_snapshot"] : [],
|
|
224
230
|
...platform === "mobile" ? [
|
|
225
231
|
"metamask.perps.clear_performance_caches",
|
|
226
232
|
"metamask.perps.measure_homepage_visible",
|
|
227
233
|
"metamask.perps.prepare_local_snapshot_endpoint"
|
|
228
234
|
] : [],
|
|
229
235
|
// read_account is core-only: only the headless core adapter implements it.
|
|
230
|
-
...platform === "core" ? ["metamask.perps.read_account"
|
|
236
|
+
...platform === "core" ? ["metamask.perps.read_account"] : []
|
|
231
237
|
];
|
|
232
238
|
const bundled = new Set(bundledActions);
|
|
233
239
|
const actions = [.../* @__PURE__ */ new Set([...bundledActions, ...declaredCustomActions])];
|
|
@@ -662,6 +668,7 @@ async function handleMobileHud(payload, context) {
|
|
|
662
668
|
}
|
|
663
669
|
}
|
|
664
670
|
async function hideMobileHudOnTeardown(projectRoot, env = {}) {
|
|
671
|
+
if (env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1") return;
|
|
665
672
|
if (process.env.METAMASK_RECIPE_AUTO_HUD === "0" || process.env.METAMASK_RECIPE_AUTO_HUD === "false") return;
|
|
666
673
|
const input = {
|
|
667
674
|
node: { bridge_timeout_ms: 8e3, cdp_timeout_ms: 5e3 },
|
|
@@ -803,7 +810,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
|
|
|
803
810
|
if (live) return live.output;
|
|
804
811
|
throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
|
|
805
812
|
}
|
|
806
|
-
if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action)) {
|
|
813
|
+
if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action) && (context.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || ["ui.swipe", "ui.pan", "ui.drag", "ui.long_press", "ui.capture_surface"].includes(action))) {
|
|
807
814
|
return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
|
|
808
815
|
}
|
|
809
816
|
return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
|
package/dist/command-contract.js
CHANGED
|
@@ -239,6 +239,22 @@ const PUBLIC_COMMAND_CONTRACTS = {
|
|
|
239
239
|
}),
|
|
240
240
|
positionals: [{ label: "platform", choices: ["ios", "android"] }]
|
|
241
241
|
},
|
|
242
|
+
"runtime-launch": {
|
|
243
|
+
options: options(HELP, JSON, TARGET, ADAPTER, DEVICE, MOBILE_PLATFORM, {
|
|
244
|
+
"--cdp-port": value(),
|
|
245
|
+
"--chrome-user-data-dir": value(),
|
|
246
|
+
"--artifacts-dir": value(),
|
|
247
|
+
"--start-watch": bool(),
|
|
248
|
+
"--build-lavamoat": bool(),
|
|
249
|
+
"--remote-flag": value(),
|
|
250
|
+
"--artifact-file": value(),
|
|
251
|
+
"--artifact-version": value(),
|
|
252
|
+
"--artifact-build-id": value(),
|
|
253
|
+
"--artifact-package-id": value(),
|
|
254
|
+
"--artifact-sha256": value(),
|
|
255
|
+
"--artifact-cache": value()
|
|
256
|
+
})
|
|
257
|
+
},
|
|
242
258
|
logs: {
|
|
243
259
|
options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, {
|
|
244
260
|
"--full": bool(),
|
package/dist/commands/call.js
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
synthesizeOneNodeRecipe,
|
|
41
41
|
validateRecipeAdapterAware
|
|
42
42
|
} from "./run-engine.js";
|
|
43
|
+
import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
43
44
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
44
45
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
45
46
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
@@ -267,7 +268,7 @@ async function handleCall(argv) {
|
|
|
267
268
|
}
|
|
268
269
|
try {
|
|
269
270
|
const prepared = await prepareHeal(adapter, target, options, json, {
|
|
270
|
-
skipMobileSourceFreshness: adapter === "mobile" && resolvedAction === "app.lifecycle"
|
|
271
|
+
skipMobileSourceFreshness: adapter === "mobile" && (resolvedAction === "app.lifecycle" || readMobileReleaseArtifactState(target) !== null)
|
|
271
272
|
});
|
|
272
273
|
if (typeof prepared === "number") return prepared;
|
|
273
274
|
const { state, heal } = prepared;
|
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
extensionProductConfigBlock,
|
|
22
22
|
extensionProductConfigFingerprint
|
|
23
23
|
} from "../../adapters/extension/product-config.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
isExtensionDistStale,
|
|
26
|
+
releaseArtifactState,
|
|
27
|
+
runtimeDistCheck
|
|
28
|
+
} from "../../adapters/extension/runtime-decision.js";
|
|
25
29
|
import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
|
|
26
30
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
27
31
|
import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
@@ -55,7 +59,18 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
55
59
|
if (process.env.CHROME_USER_DATA_DIR) {
|
|
56
60
|
process.env.CHROME_USER_DATA_DIR = path.resolve(process.env.CHROME_USER_DATA_DIR);
|
|
57
61
|
}
|
|
58
|
-
const
|
|
62
|
+
const artifact = releaseArtifactState(target);
|
|
63
|
+
const artifactSnapshot = runtimeDistCheck(target, artifact);
|
|
64
|
+
const artifactLaunchable = !wantWatch && tier !== "build" && artifact.status === "valid" && artifactSnapshot.status === "fresh";
|
|
65
|
+
const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
|
|
66
|
+
if (artifact.status !== "none" && !reusable && !artifactLaunchable) {
|
|
67
|
+
const detail = artifact.status === "invalid" ? `identity is invalid: ${artifact.reason ?? "unknown reason"}` : "runtime is not healthy and reusable";
|
|
68
|
+
return {
|
|
69
|
+
status: EXIT.infra,
|
|
70
|
+
output: `RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact ${detail}.
|
|
71
|
+
Next: rerun runtime-launch with the same artifact source.`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
59
74
|
if (!reusable) {
|
|
60
75
|
const block = extensionProductConfigBlock(target);
|
|
61
76
|
if (block) {
|
|
@@ -80,14 +95,23 @@ Next: ${block.userAction}`
|
|
|
80
95
|
if (reusable) {
|
|
81
96
|
return extensionReattach(target, displayMode);
|
|
82
97
|
}
|
|
98
|
+
if (artifactLaunchable) {
|
|
99
|
+
return extensionLaunchReleaseArtifact(target, artifact);
|
|
100
|
+
}
|
|
83
101
|
return extensionLaunchDevelopment(target);
|
|
84
102
|
}
|
|
85
|
-
async function extensionRuntimeReusable(target) {
|
|
103
|
+
async function extensionRuntimeReusable(target, artifact = releaseArtifactState(target)) {
|
|
86
104
|
const cdpPort = process.env.CDP_PORT;
|
|
87
105
|
if (!cdpPort) return false;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
106
|
+
const runtimeSnapshot = runtimeDistCheck(target, artifact);
|
|
107
|
+
const releaseArtifact = runtimeSnapshot.source === "release-artifact";
|
|
108
|
+
if (releaseArtifact) {
|
|
109
|
+
if (runtimeSnapshot.status !== "fresh") return false;
|
|
110
|
+
} else {
|
|
111
|
+
if (isExtensionDistStale(target)) return false;
|
|
112
|
+
if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
|
|
113
|
+
if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
|
|
114
|
+
}
|
|
91
115
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
92
116
|
const reachable = await cdpVersionReachable(cdpPort);
|
|
93
117
|
const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
|
|
@@ -344,6 +368,31 @@ async function extensionLaunchDevelopment(target) {
|
|
|
344
368
|
));
|
|
345
369
|
return spawnScriptStreaming(liveScript, liveArgs, target);
|
|
346
370
|
}
|
|
371
|
+
async function extensionLaunchReleaseArtifact(target, artifact) {
|
|
372
|
+
if (!artifact.sourceDir || !artifact.provenancePath) {
|
|
373
|
+
return {
|
|
374
|
+
status: EXIT.infra,
|
|
375
|
+
output: "RELEASE_ARTIFACT_NOT_REUSABLE: The loaded release artifact identity is incomplete.\nNext: rerun runtime-launch with the same artifact source."
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
379
|
+
const liveArgs = [
|
|
380
|
+
"--target",
|
|
381
|
+
target,
|
|
382
|
+
"--external-artifact",
|
|
383
|
+
"--dist-dir",
|
|
384
|
+
artifact.sourceDir,
|
|
385
|
+
"--artifact-provenance",
|
|
386
|
+
artifact.provenancePath,
|
|
387
|
+
"--preserve-profile"
|
|
388
|
+
];
|
|
389
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
390
|
+
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
391
|
+
console.error(colorHumanMessage(
|
|
392
|
+
`\u2192 extension release relaunch \u2014 verified artifact ${artifact.manifestVersion ?? artifact.expectedVersion ?? ""} \xB7 CDP :${process.env.CDP_PORT ?? "default"}`
|
|
393
|
+
));
|
|
394
|
+
return spawnScriptStreaming(liveScript, liveArgs, target);
|
|
395
|
+
}
|
|
347
396
|
function harnessWatcherMatchesProductConfig(target) {
|
|
348
397
|
const runtimeDir = path.join(target, recipeRuntimeDir());
|
|
349
398
|
const harnessPidFile = path.join(runtimeDir, "recipe-harness-webpack.pid");
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
recordMobileSourceBaseline
|
|
5
5
|
} from "../../adapters/mobile/source-freshness.js";
|
|
6
6
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
7
|
+
import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
|
|
7
8
|
import { EXIT } from "../shared.js";
|
|
8
9
|
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false) {
|
|
10
|
+
clearMobileReleaseArtifactState(target);
|
|
9
11
|
await ensureHarnessFresh(target, "mobile");
|
|
10
12
|
const platform = mobileTarget ?? "ios";
|
|
11
13
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { detectAdapter } from "../harness.js";
|
|
2
2
|
import { assertAdapter } from "../paths.js";
|
|
3
3
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
+
import { clearMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
4
5
|
import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
|
|
5
6
|
import {
|
|
6
7
|
applyWatcherPortOption,
|
|
@@ -28,6 +29,7 @@ async function handleProvision({ positional, options, rawArgv }) {
|
|
|
28
29
|
const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
|
|
29
30
|
if (adapter === "mobile") {
|
|
30
31
|
writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
|
|
32
|
+
if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
|
|
31
33
|
}
|
|
32
34
|
const result = await surface.runwayProvision.run(target, {
|
|
33
35
|
json,
|