@deeeed/metamask-harness 0.73.0 → 0.75.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 +22 -0
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +8 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +17 -1
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +19 -1
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +17 -0
- package/dist/adapters/surface.js +6 -11
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/launch.js +85 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +9 -1
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/launch/device.js +342 -0
- package/dist/commands/launch/index.js +9 -942
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/pin.js +226 -0
- package/dist/commands/review.js +3 -1
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/commands/shared.js +2 -2
- package/dist/doctor.js +3 -1
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +27 -2
- package/dist/network-observation.js +10 -3
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/docs/CONTRIBUTING.md +20 -3
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { isPin, PIN_MARKER } from "../harness-pin.js";
|
|
8
|
+
import { runnerDir } from "../paths.js";
|
|
9
|
+
import { EXIT } from "./shared.js";
|
|
10
|
+
const PACKAGE_NAME = "@deeeed/metamask-harness";
|
|
11
|
+
const STALE_STAGING_MS = 30 * 60 * 1e3;
|
|
12
|
+
const RUNNER_STATE = /* @__PURE__ */ new Set([".omc", ".omx"]);
|
|
13
|
+
function pinRoot(env = process.env, home = os.homedir()) {
|
|
14
|
+
return path.resolve(env.MM_HARNESS_PIN_ROOT || path.join(home, ".local/share/mm-harness/pins"));
|
|
15
|
+
}
|
|
16
|
+
function readManifest(root) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
18
|
+
}
|
|
19
|
+
function contentDigest(root) {
|
|
20
|
+
const hash = crypto.createHash("sha256");
|
|
21
|
+
const walk = (directory, relative) => {
|
|
22
|
+
hash.update(`d\0${relative}\0`);
|
|
23
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (RUNNER_STATE.has(entry.name) || !relative && entry.name === PIN_MARKER) continue;
|
|
26
|
+
const absolute = path.join(directory, entry.name);
|
|
27
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
walk(absolute, child);
|
|
30
|
+
} else if (entry.isSymbolicLink()) {
|
|
31
|
+
hash.update(`l\0${child}\0${fs.readlinkSync(absolute)}\0`);
|
|
32
|
+
} else if (entry.isFile()) {
|
|
33
|
+
const fd = fs.openSync(absolute, "r");
|
|
34
|
+
try {
|
|
35
|
+
const opened = fs.fstatSync(fd);
|
|
36
|
+
hash.update(`f\0${child}\0${opened.mode & 73 ? "x" : "-"}\0${opened.size}\0`);
|
|
37
|
+
hash.update(fs.readFileSync(fd));
|
|
38
|
+
hash.update("\0");
|
|
39
|
+
} finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(root, "");
|
|
46
|
+
return hash.digest("hex");
|
|
47
|
+
}
|
|
48
|
+
function relinkIntoCopy(copy, sourceAliases) {
|
|
49
|
+
const walk = (directory) => {
|
|
50
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
51
|
+
const absolute = path.join(directory, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
walk(absolute);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isSymbolicLink()) continue;
|
|
57
|
+
const raw = fs.readlinkSync(absolute);
|
|
58
|
+
if (!path.isAbsolute(raw)) continue;
|
|
59
|
+
const targets = [path.resolve(raw)];
|
|
60
|
+
try {
|
|
61
|
+
targets.push(fs.realpathSync(raw));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
let match;
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
const alias = sourceAliases.find((prefix) => target === prefix || target.startsWith(`${prefix}${path.sep}`));
|
|
67
|
+
if (alias) {
|
|
68
|
+
match = { alias, target };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
fs.unlinkSync(absolute);
|
|
74
|
+
fs.symlinkSync(path.relative(directory, path.join(copy, path.relative(match.alias, match.target))) || ".", absolute);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(copy);
|
|
78
|
+
}
|
|
79
|
+
function assertSelfContained(root) {
|
|
80
|
+
const manifest = readManifest(root);
|
|
81
|
+
const lookup = createRequire(path.join(root, "package.json")).resolve.paths("") ?? [];
|
|
82
|
+
const realRoot = fs.realpathSync(root);
|
|
83
|
+
const inside = (candidate) => candidate === realRoot || candidate.startsWith(`${realRoot}${path.sep}`);
|
|
84
|
+
for (const dependency of Object.keys(manifest.dependencies ?? {})) {
|
|
85
|
+
const found = lookup.map((directory) => path.join(directory, dependency)).find((candidate) => fs.existsSync(path.join(candidate, "package.json")));
|
|
86
|
+
if (!found) throw new Error(`dependency ${dependency} is not installed for ${root}`);
|
|
87
|
+
const resolved = fs.realpathSync(found);
|
|
88
|
+
if (!inside(resolved)) {
|
|
89
|
+
throw new Error(`dependency ${dependency} resolves outside the install (${resolved}); only a self-contained install (npm i -g) can be pinned`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const walk = (directory) => {
|
|
93
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
94
|
+
const absolute = path.join(directory, entry.name);
|
|
95
|
+
if (entry.isDirectory()) walk(absolute);
|
|
96
|
+
else if (entry.isSymbolicLink() && path.isAbsolute(fs.readlinkSync(absolute))) {
|
|
97
|
+
let resolved;
|
|
98
|
+
try {
|
|
99
|
+
resolved = fs.realpathSync(absolute);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!inside(resolved)) throw new Error(`link ${path.relative(root, absolute)} leads outside the install (${resolved})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(root);
|
|
108
|
+
}
|
|
109
|
+
function readPinMarker(pin) {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(fs.readFileSync(path.join(pin, PIN_MARKER), "utf8"));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === "ENOENT") return void 0;
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function assertPinUnchanged(pin, marker) {
|
|
118
|
+
if (typeof marker.contentSha256 !== "string" || marker.contentSha256 !== contentDigest(pin)) {
|
|
119
|
+
throw new Error(`pin ${pin} changed after it was created; remove it, then re-run: mm-harness pin`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function sweepStaleStaging(store, now = Date.now()) {
|
|
123
|
+
let names;
|
|
124
|
+
try {
|
|
125
|
+
names = fs.readdirSync(store);
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
const created = /^\.staging-.+-(\d{13})-[^-]+$/u.exec(name)?.[1];
|
|
131
|
+
if (created && now - Number(created) > STALE_STAGING_MS) {
|
|
132
|
+
fs.rmSync(path.join(store, name), { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function pinHarness(options = {}) {
|
|
137
|
+
const sourceAlias = path.resolve(options.sourceRoot ?? runnerDir);
|
|
138
|
+
const source = fs.realpathSync(sourceAlias);
|
|
139
|
+
const store = options.root ?? pinRoot();
|
|
140
|
+
const manifest = readManifest(source);
|
|
141
|
+
if (manifest.name !== PACKAGE_NAME || typeof manifest.version !== "string" || !manifest.version) {
|
|
142
|
+
throw new Error(`not an ${PACKAGE_NAME} package: ${source}`);
|
|
143
|
+
}
|
|
144
|
+
const version = manifest.version;
|
|
145
|
+
const ownExecutable = path.join(source, "bin/mm-harness");
|
|
146
|
+
if (isPin(source)) {
|
|
147
|
+
return { version, source, executable: ownExecutable, pinned: true, created: false, reason: "already-pinned" };
|
|
148
|
+
}
|
|
149
|
+
if (!source.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
150
|
+
return { version, source, executable: ownExecutable, pinned: false, created: false, reason: "source-checkout" };
|
|
151
|
+
}
|
|
152
|
+
const key = `${version}-${contentDigest(source).slice(0, 12)}`;
|
|
153
|
+
const target = path.join(store, key);
|
|
154
|
+
const executable = path.join(target, "bin/mm-harness");
|
|
155
|
+
const existing = readPinMarker(target);
|
|
156
|
+
if (existing) {
|
|
157
|
+
assertPinUnchanged(target, existing);
|
|
158
|
+
return { version, source, executable, pinned: true, created: false };
|
|
159
|
+
}
|
|
160
|
+
assertSelfContained(source);
|
|
161
|
+
fs.mkdirSync(store, { recursive: true });
|
|
162
|
+
sweepStaleStaging(store);
|
|
163
|
+
const staging = fs.mkdtempSync(path.join(store, `.staging-${key}-${Date.now()}-`));
|
|
164
|
+
const copy = path.join(staging, "package");
|
|
165
|
+
try {
|
|
166
|
+
fs.cpSync(source, copy, {
|
|
167
|
+
recursive: true,
|
|
168
|
+
preserveTimestamps: true,
|
|
169
|
+
verbatimSymlinks: true,
|
|
170
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
171
|
+
filter: (from) => !RUNNER_STATE.has(path.basename(from))
|
|
172
|
+
});
|
|
173
|
+
relinkIntoCopy(copy, [.../* @__PURE__ */ new Set([source, sourceAlias])]);
|
|
174
|
+
assertSelfContained(copy);
|
|
175
|
+
const check = spawnSync(path.join(copy, "bin/mm-harness"), ["--version"], {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
env: { ...process.env, MM_HARNESS_BIN: "", MM_HARNESS_NO_UPDATE_CHECK: "1" },
|
|
178
|
+
timeout: 6e4
|
|
179
|
+
});
|
|
180
|
+
if (check.status !== 0 || check.stdout.trim() !== version) {
|
|
181
|
+
throw new Error(`the copied harness did not run (exit ${check.status}): ${(check.stderr || check.stdout || "").trim()}`);
|
|
182
|
+
}
|
|
183
|
+
const marker = { schemaVersion: 1, key, version, source, contentSha256: contentDigest(copy), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
184
|
+
fs.writeFileSync(path.join(copy, PIN_MARKER), `${JSON.stringify(marker, null, 2)}
|
|
185
|
+
`);
|
|
186
|
+
try {
|
|
187
|
+
fs.renameSync(copy, target);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (!isPin(target)) throw error;
|
|
190
|
+
return { version, source, executable, pinned: true, created: false };
|
|
191
|
+
}
|
|
192
|
+
return { version, source, executable, pinned: true, created: true };
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function handlePin(argv) {
|
|
198
|
+
const json = argv.includes("--json");
|
|
199
|
+
try {
|
|
200
|
+
const result = pinHarness();
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "pin", status: "pass", ...result }, null, 2));
|
|
203
|
+
} else {
|
|
204
|
+
console.log(result.executable);
|
|
205
|
+
if (result.reason === "source-checkout") {
|
|
206
|
+
console.error("mm-harness pin: a source checkout is not copied; record it by its working-tree hash.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return EXIT.ok;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
const userAction = "install the harness globally (npm i -g @deeeed/metamask-harness), then re-run: mm-harness pin";
|
|
213
|
+
if (json) {
|
|
214
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "pin", status: "fail", error: { code: "PIN_FAILED", message, userAction } }, null, 2));
|
|
215
|
+
} else {
|
|
216
|
+
console.error(`\u2717 mm-harness pin: ${message}
|
|
217
|
+
Next: ${userAction}`);
|
|
218
|
+
}
|
|
219
|
+
return EXIT.runtime;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
handlePin,
|
|
224
|
+
pinHarness,
|
|
225
|
+
pinRoot
|
|
226
|
+
};
|
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);
|
|
@@ -14,9 +14,6 @@ import {
|
|
|
14
14
|
import { resolveActionManifest, validateManifest } from "../manifest.js";
|
|
15
15
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
16
16
|
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
17
|
-
import {
|
|
18
|
-
requireCurrentMobileReleaseArtifactState
|
|
19
|
-
} from "../adapters/mobile/release-artifact-state.js";
|
|
20
17
|
import {
|
|
21
18
|
importRecipeHarness,
|
|
22
19
|
importRecipeProtocol,
|
|
@@ -121,13 +118,8 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
120
|
} finally {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await hideMobileHudOnTeardown(
|
|
127
|
-
projectRoot,
|
|
128
|
-
recipeRunEnv(adapter, runtimeOptions)
|
|
129
|
-
);
|
|
130
|
-
}
|
|
121
|
+
const teardown = getAdapterSurface(adapter).run?.teardown;
|
|
122
|
+
if (teardown) await teardown(projectRoot, recipeRunEnv(adapter, runtimeOptions));
|
|
131
123
|
}
|
|
132
124
|
let recordingError;
|
|
133
125
|
if (executionError === void 0 && result) {
|
|
@@ -240,11 +232,12 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
240
232
|
projectRoot,
|
|
241
233
|
rootRecipe: recipeDocument
|
|
242
234
|
});
|
|
243
|
-
const
|
|
235
|
+
const surface = getAdapterSurface(adapter);
|
|
236
|
+
const suppressAutoHud = surface.run?.autoHud?.() === false;
|
|
244
237
|
const runnerOptions = {
|
|
245
238
|
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
246
239
|
suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
|
|
247
|
-
autoHud:
|
|
240
|
+
autoHud: suppressAutoHud ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
248
241
|
onActionEvent: runtimeOptions.onActionEvent,
|
|
249
242
|
actionSources,
|
|
250
243
|
// A direct engineer invocation is the explicit trust boundary for a
|
|
@@ -256,7 +249,7 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
|
|
|
256
249
|
...runnerOptions,
|
|
257
250
|
trustedMutation
|
|
258
251
|
});
|
|
259
|
-
const useFramedExtensionRecording =
|
|
252
|
+
const useFramedExtensionRecording = surface.recording?.framed !== void 0 && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
260
253
|
const runRequest = {
|
|
261
254
|
...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
262
255
|
artifactsDir: absoluteArtifactsDir,
|
|
@@ -447,31 +440,22 @@ function executionLibrarySources(sources, invocationTrust) {
|
|
|
447
440
|
};
|
|
448
441
|
});
|
|
449
442
|
}
|
|
443
|
+
const RUN_ENV_KEYS = [
|
|
444
|
+
"CDP_PORT",
|
|
445
|
+
"RECIPE_CDP_PORT",
|
|
446
|
+
"WATCHER_PORT",
|
|
447
|
+
"METRO_PORT",
|
|
448
|
+
"METAMASK_RECIPE_EXTENSION_AUTOLAUNCH"
|
|
449
|
+
];
|
|
450
450
|
function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const previousAndroidSerial = process.env.ANDROID_SERIAL;
|
|
460
|
-
const explicitMobileDeviceEnv = adapter === "mobile" && process.env.MM_HARNESS_EXPLICIT_PLATFORM ? Object.fromEntries(
|
|
461
|
-
[
|
|
462
|
-
"PLATFORM",
|
|
463
|
-
"MM_HARNESS_EXPLICIT_PLATFORM",
|
|
464
|
-
"IOS_SIMULATOR",
|
|
465
|
-
"SIM_UDID",
|
|
466
|
-
"ADB_SERIAL",
|
|
467
|
-
"ANDROID_SERIAL",
|
|
468
|
-
"ANDROID_DEVICE",
|
|
469
|
-
"ANDROID_TARGET_DEVICE_NAME"
|
|
470
|
-
].map((key) => [key, process.env[key]])
|
|
471
|
-
) : void 0;
|
|
472
|
-
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
473
|
-
if (explicitMobileDeviceEnv) {
|
|
474
|
-
for (const [key, value] of Object.entries(explicitMobileDeviceEnv)) {
|
|
451
|
+
const surface = getAdapterSurface(adapter);
|
|
452
|
+
const previousEnv = Object.fromEntries(
|
|
453
|
+
[...RUN_ENV_KEYS, ...surface.run?.envKeys ?? []].map((key) => [key, process.env[key]])
|
|
454
|
+
);
|
|
455
|
+
const pinnedEnv = surface.run?.pinnedEnv?.();
|
|
456
|
+
surface.resolveSlotPorts(projectRoot);
|
|
457
|
+
if (pinnedEnv) {
|
|
458
|
+
for (const [key, value] of Object.entries(pinnedEnv)) {
|
|
475
459
|
restoreEnv(key, value);
|
|
476
460
|
}
|
|
477
461
|
}
|
|
@@ -486,37 +470,9 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
|
|
|
486
470
|
if (runtimeOptions.launchExistingDist) {
|
|
487
471
|
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
488
472
|
}
|
|
489
|
-
|
|
490
|
-
const state = requireCurrentMobileReleaseArtifactState(projectRoot);
|
|
491
|
-
if (state) {
|
|
492
|
-
const selected = process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
|
|
493
|
-
if (selected && selected !== state.deviceSerial) {
|
|
494
|
-
throw new Error(
|
|
495
|
-
`The Mobile release artifact is installed on ${state.deviceSerial}, not ${selected}. Next: rerun runtime-launch for ${selected}.`
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME = "1";
|
|
499
|
-
process.env.ANDROID_PACKAGE_ID = state.packageId;
|
|
500
|
-
process.env.ADB_SERIAL = state.deviceSerial;
|
|
501
|
-
process.env.ANDROID_SERIAL = state.deviceSerial;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
473
|
+
surface.run?.activateEnv?.(projectRoot);
|
|
504
474
|
return () => {
|
|
505
|
-
restoreEnv(
|
|
506
|
-
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
507
|
-
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
508
|
-
restoreEnv("METRO_PORT", previousMetroPort);
|
|
509
|
-
restoreEnv(
|
|
510
|
-
"METAMASK_RECIPE_EXTENSION_AUTOLAUNCH",
|
|
511
|
-
previousExtensionAutolaunch
|
|
512
|
-
);
|
|
513
|
-
restoreEnv(
|
|
514
|
-
"METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME",
|
|
515
|
-
previousMobileOpaqueRuntime
|
|
516
|
-
);
|
|
517
|
-
restoreEnv("ANDROID_PACKAGE_ID", previousAndroidPackageId);
|
|
518
|
-
restoreEnv("ADB_SERIAL", previousAdbSerial);
|
|
519
|
-
restoreEnv("ANDROID_SERIAL", previousAndroidSerial);
|
|
475
|
+
for (const [key, value] of Object.entries(previousEnv)) restoreEnv(key, value);
|
|
520
476
|
};
|
|
521
477
|
}
|
|
522
478
|
function actionSourceMap(sources) {
|
|
@@ -539,42 +495,11 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
|
539
495
|
PLATFORM: process.env.PLATFORM,
|
|
540
496
|
MM_HARNESS_EXPLICIT_PLATFORM: process.env.MM_HARNESS_EXPLICIT_PLATFORM
|
|
541
497
|
};
|
|
542
|
-
|
|
543
|
-
return
|
|
544
|
-
...base,
|
|
545
|
-
WATCHER_PORT: process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
546
|
-
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
|
|
547
|
-
IOS_SIMULATOR: process.env.IOS_SIMULATOR,
|
|
548
|
-
SIM_UDID: process.env.SIM_UDID,
|
|
549
|
-
ANDROID_DEVICE: process.env.ANDROID_DEVICE,
|
|
550
|
-
ANDROID_TARGET_DEVICE_NAME: process.env.ANDROID_TARGET_DEVICE_NAME,
|
|
551
|
-
ADB_SERIAL: process.env.ADB_SERIAL,
|
|
552
|
-
ANDROID_SERIAL: process.env.ANDROID_SERIAL,
|
|
553
|
-
ANDROID_PACKAGE_ID: process.env.ANDROID_PACKAGE_ID,
|
|
554
|
-
METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME: process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME
|
|
555
|
-
};
|
|
498
|
+
const childEnv = getAdapterSurface(adapter).run?.childEnv;
|
|
499
|
+
return childEnv ? childEnv(base) : base;
|
|
556
500
|
}
|
|
557
501
|
async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
|
|
558
|
-
|
|
559
|
-
return;
|
|
560
|
-
const { releaseArtifactState, runtimeDistCheck } = await import("../adapters/extension/runtime-decision.js");
|
|
561
|
-
const artifact = releaseArtifactState(projectRoot);
|
|
562
|
-
const runtimeSnapshot = runtimeDistCheck(projectRoot, artifact);
|
|
563
|
-
if (artifact.status !== "none") {
|
|
564
|
-
if (artifact.status === "valid" && runtimeSnapshot.status === "fresh") return;
|
|
565
|
-
throw new Error(
|
|
566
|
-
"The loaded release artifact identity is invalid or stale. Next: rerun runtime-launch with the same artifact source."
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
const { prepareExtensionRuntime } = await import("../adapters/extension/runtime.js");
|
|
570
|
-
await prepareExtensionRuntime({
|
|
571
|
-
projectRoot,
|
|
572
|
-
cdpPort: runtimeOptions.cdpPort,
|
|
573
|
-
slot: runtimeOptions.slot,
|
|
574
|
-
launchExistingDist: runtimeOptions.launchExistingDist === true,
|
|
575
|
-
validationRuntimeDir: runtimeOptions.validationRuntimeDir,
|
|
576
|
-
pageMode: "home-or-sidepanel"
|
|
577
|
-
});
|
|
502
|
+
await getAdapterSurface(adapter).run?.prepareRuntime?.(projectRoot, runtimeOptions);
|
|
578
503
|
}
|
|
579
504
|
function restoreEnv(key, value) {
|
|
580
505
|
if (value === void 0) delete process.env[key];
|
|
@@ -1051,7 +976,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
1051
976
|
return EXIT.usage;
|
|
1052
977
|
}
|
|
1053
978
|
const state = newHealState();
|
|
1054
|
-
const
|
|
979
|
+
const runtimeCheck = getAdapterSurface(adapter).run?.runtimeCheck?.({
|
|
980
|
+
target,
|
|
981
|
+
options,
|
|
982
|
+
heal,
|
|
983
|
+
state,
|
|
984
|
+
json,
|
|
985
|
+
appRestartAuthored: prepareOptions.appRestartAuthored,
|
|
986
|
+
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
987
|
+
});
|
|
1055
988
|
prepareOptions.onPhase?.("install");
|
|
1056
989
|
const ensured = await ensureOverlay(
|
|
1057
990
|
adapter,
|
|
@@ -1089,41 +1022,8 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
1089
1022
|
return EXIT.infra;
|
|
1090
1023
|
}
|
|
1091
1024
|
prepareOptions.onPhase?.("healthcheck");
|
|
1092
|
-
if (
|
|
1093
|
-
const
|
|
1094
|
-
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
1095
|
-
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
1096
|
-
const explicitCdpPort = optionString(options, "cdpPort");
|
|
1097
|
-
const explicitWatcherPort = optionString(options, "watcherPort") ?? optionString(options, "metroPort");
|
|
1098
|
-
if (explicitCdpPort) {
|
|
1099
|
-
process.env.CDP_PORT = explicitCdpPort;
|
|
1100
|
-
process.env.RECIPE_CDP_PORT = explicitCdpPort;
|
|
1101
|
-
}
|
|
1102
|
-
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
1103
|
-
try {
|
|
1104
|
-
const current = await ensureExtensionProofRuntime(
|
|
1105
|
-
target,
|
|
1106
|
-
heal,
|
|
1107
|
-
state,
|
|
1108
|
-
json,
|
|
1109
|
-
{
|
|
1110
|
-
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
1111
|
-
}
|
|
1112
|
-
);
|
|
1113
|
-
if (current !== null) return current;
|
|
1114
|
-
} finally {
|
|
1115
|
-
restoreEnv("CDP_PORT", previousCdpPort);
|
|
1116
|
-
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
1117
|
-
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
if (adapter === "mobile" && mobileArtifact === null && prepareOptions.skipMobileSourceFreshness !== true) {
|
|
1121
|
-
const current = await ensureMobileProofRuntime(
|
|
1122
|
-
target,
|
|
1123
|
-
heal,
|
|
1124
|
-
state,
|
|
1125
|
-
json
|
|
1126
|
-
);
|
|
1025
|
+
if (runtimeCheck) {
|
|
1026
|
+
const current = await runtimeCheck();
|
|
1127
1027
|
if (current !== null) return current;
|
|
1128
1028
|
}
|
|
1129
1029
|
return { state, heal };
|
|
@@ -1372,7 +1272,7 @@ async function executeWithHealBounds(exec, target, state) {
|
|
|
1372
1272
|
};
|
|
1373
1273
|
}
|
|
1374
1274
|
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
1375
|
-
const userAction = adapter
|
|
1275
|
+
const userAction = (adapter ? getAdapterSurface(adapter).run?.violationUserAction?.(violation) : void 0) ?? violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
1376
1276
|
if (json) {
|
|
1377
1277
|
console.log(
|
|
1378
1278
|
JSON.stringify(
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { execFileSync } from "node:child_process";
|
|
5
|
-
import {
|
|
4
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
6
5
|
import { isRecord } from "./parse-args.js";
|
|
7
6
|
function writeRunReport(result) {
|
|
8
7
|
const summaryPath = String(result.summaryPath);
|
|
@@ -95,33 +94,6 @@ function indexProductProvenanceArtifact(artifactManifestPath, target, adapter, b
|
|
|
95
94
|
`
|
|
96
95
|
);
|
|
97
96
|
}
|
|
98
|
-
function readRecord(file) {
|
|
99
|
-
try {
|
|
100
|
-
const recorded = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
101
|
-
return isRecord(recorded) ? recorded : null;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const browserCdp = createRequire(import.meta.url)("../../adapters/shared/browser-cdp.cjs");
|
|
107
|
-
function verifyLaunch(record, cdpPort) {
|
|
108
|
-
const launch = isRecord(record.launch) ? record.launch : {};
|
|
109
|
-
const pid = Number(launch.pid);
|
|
110
|
-
const port = Number(launch.cdpPort);
|
|
111
|
-
if (!Number.isInteger(pid) || !Number.isInteger(port) || typeof launch.profile !== "string" || typeof launch.processStartedAt !== "string") {
|
|
112
|
-
return "the browser record has no process identity (pid, start time, profile, CDP port)";
|
|
113
|
-
}
|
|
114
|
-
if (cdpPort && port !== Number(cdpPort)) return `the browser record is for CDP port ${port}, not ${cdpPort}`;
|
|
115
|
-
try {
|
|
116
|
-
const owner = browserCdp.cdpOwner(port, launch.profile);
|
|
117
|
-
if (owner.pid !== pid || owner.startedAt !== launch.processStartedAt) {
|
|
118
|
-
return `CDP port ${port} is now held by another browser process than the recorded pid ${pid}`;
|
|
119
|
-
}
|
|
120
|
-
} catch (error) {
|
|
121
|
-
return error instanceof Error ? error.message : String(error);
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
97
|
function resolveTemplatedPort(value, params, recipe) {
|
|
126
98
|
let resolved = value;
|
|
127
99
|
const template = /^\{\{\s*params\.([\w-]+)\s*\}\}$/u.exec(String(value).trim());
|
|
@@ -155,26 +127,11 @@ function recipeCdpPorts(request) {
|
|
|
155
127
|
return [...ports];
|
|
156
128
|
}
|
|
157
129
|
function executedBrowser(adapter, target, artifactManifestPath, ports) {
|
|
158
|
-
|
|
130
|
+
const launched = getAdapterSurface(adapter).run?.launchedBrowser;
|
|
131
|
+
if (!launched) return null;
|
|
159
132
|
if (ports.length === 0) return { boundTo: "none", reason: "the run had no known CDP port" };
|
|
160
133
|
if (ports.length > 1) return { boundTo: "none", reason: `the run drove several CDP ports (${ports.join(", ")})` };
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
function launchedBrowser(target, artifactsDir, cdpPort) {
|
|
164
|
-
const runtime = readRecord(path.join(artifactsDir, "extension-runtime", "runtime.json"));
|
|
165
|
-
const autolaunched = runtime && isRecord(runtime.browser) ? runtime.browser : null;
|
|
166
|
-
const candidates = [];
|
|
167
|
-
if (autolaunched) candidates.push([autolaunched, "run-autolaunch"]);
|
|
168
|
-
const slot = readRecord(path.join(path.resolve(target), recipeRuntimeDir(), "browser-resolution.json"));
|
|
169
|
-
if (slot) candidates.push([slot, "slot-runtime"]);
|
|
170
|
-
if (candidates.length === 0) return null;
|
|
171
|
-
const reasons = [];
|
|
172
|
-
for (const [record, boundTo] of candidates) {
|
|
173
|
-
const problem = verifyLaunch(record, cdpPort);
|
|
174
|
-
if (problem === null) return { ...record, boundTo };
|
|
175
|
-
reasons.push(`${boundTo}: ${problem}`);
|
|
176
|
-
}
|
|
177
|
-
return { boundTo: "none", reason: reasons.join("; ") };
|
|
134
|
+
return launched(target, path.dirname(artifactManifestPath), ports[0]);
|
|
178
135
|
}
|
|
179
136
|
function renderRunReport(summary, entries) {
|
|
180
137
|
const lines = [
|
|
@@ -279,7 +236,6 @@ function indexRunReportArtifact(artifactManifestPath) {
|
|
|
279
236
|
export {
|
|
280
237
|
executedBrowser,
|
|
281
238
|
indexProductProvenanceArtifact,
|
|
282
|
-
launchedBrowser,
|
|
283
239
|
recipeCdpPorts,
|
|
284
240
|
writeRunReport
|
|
285
241
|
};
|
package/dist/commands/run.js
CHANGED
|
@@ -28,8 +28,6 @@ import {
|
|
|
28
28
|
import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
|
|
29
29
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
30
30
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
31
|
-
import { coreDependencyBlock } from "./core-readiness.js";
|
|
32
|
-
import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
33
31
|
import {
|
|
34
32
|
indexProductProvenanceArtifact,
|
|
35
33
|
writeRunReport
|
|
@@ -244,7 +242,10 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
244
242
|
actionLibraryContextArgs(validated.librarySources)
|
|
245
243
|
);
|
|
246
244
|
}
|
|
247
|
-
const depsBlock =
|
|
245
|
+
const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, {
|
|
246
|
+
recipe: validated.recipe,
|
|
247
|
+
librarySources: validated.librarySources
|
|
248
|
+
}) ?? null;
|
|
248
249
|
if (depsBlock) {
|
|
249
250
|
return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
|
|
250
251
|
}
|
|
@@ -295,8 +296,7 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
295
296
|
}
|
|
296
297
|
try {
|
|
297
298
|
const prepared = await prepareHeal(adapter, target, options, machine, {
|
|
298
|
-
onPhase: (phase, fields) => stream.phase(phase, fields)
|
|
299
|
-
skipMobileSourceFreshness: adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
|
|
299
|
+
onPhase: (phase, fields) => stream.phase(phase, fields)
|
|
300
300
|
});
|
|
301
301
|
if (typeof prepared === "number") {
|
|
302
302
|
stream.error({
|
|
@@ -511,48 +511,6 @@ function formatPreviewLine(line, out) {
|
|
|
511
511
|
const rest = match[2];
|
|
512
512
|
return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
|
|
513
513
|
}
|
|
514
|
-
async function recipeUsesCoreController(recipe, librarySources) {
|
|
515
|
-
if (objectUsesCoreController(recipe)) return true;
|
|
516
|
-
const refs = collectCallRefs(recipe);
|
|
517
|
-
if (refs.length === 0) return false;
|
|
518
|
-
const sources = librarySources ?? [];
|
|
519
|
-
if (sources.length === 0) return false;
|
|
520
|
-
const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
|
|
521
|
-
const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
|
|
522
|
-
const visited = /* @__PURE__ */ new Set();
|
|
523
|
-
const visit = (ref) => {
|
|
524
|
-
if (visited.has(ref)) return false;
|
|
525
|
-
visited.add(ref);
|
|
526
|
-
const dependency = recipes.get(ref);
|
|
527
|
-
if (!dependency) return false;
|
|
528
|
-
if (objectUsesCoreController(dependency.document)) return true;
|
|
529
|
-
return collectCallRefs(dependency.document).some(visit);
|
|
530
|
-
};
|
|
531
|
-
return refs.some(visit);
|
|
532
|
-
}
|
|
533
|
-
function objectUsesCoreController(value) {
|
|
534
|
-
if (Array.isArray(value)) return value.some(objectUsesCoreController);
|
|
535
|
-
if (!isRecord(value)) return false;
|
|
536
|
-
if (actionUsesCoreController(value.action)) return true;
|
|
537
|
-
return Object.values(value).some(objectUsesCoreController);
|
|
538
|
-
}
|
|
539
|
-
function collectCallRefs(value) {
|
|
540
|
-
const refs = [];
|
|
541
|
-
const visit = (node) => {
|
|
542
|
-
if (Array.isArray(node)) {
|
|
543
|
-
for (const item of node) visit(item);
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
if (!isRecord(node)) return;
|
|
547
|
-
if (node.action === "call" && typeof node.ref === "string" && node.ref) refs.push(node.ref);
|
|
548
|
-
for (const child of Object.values(node)) visit(child);
|
|
549
|
-
};
|
|
550
|
-
visit(value);
|
|
551
|
-
return [...new Set(refs)];
|
|
552
|
-
}
|
|
553
|
-
function actionUsesCoreController(action) {
|
|
554
|
-
return typeof action === "string" && action.startsWith("metamask.perps.");
|
|
555
|
-
}
|
|
556
514
|
async function handleRunPlan(recipeArg, params, options, stream) {
|
|
557
515
|
const json = optionFlag(options, "json");
|
|
558
516
|
const jsonOutput = json && !stream.enabled;
|
|
@@ -635,7 +593,7 @@ async function handleRunPlan(recipeArg, params, options, stream) {
|
|
|
635
593
|
status: "planned",
|
|
636
594
|
detail: "would auto-ensure the runtime overlay if missing (install phase)"
|
|
637
595
|
},
|
|
638
|
-
...adapter
|
|
596
|
+
...getAdapterSurface(adapter).headless ? [] : [
|
|
639
597
|
{
|
|
640
598
|
step: "launch.app",
|
|
641
599
|
confidence: "conditional",
|