@deeeed/metamask-harness 0.74.0 → 0.76.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 +39 -1
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/adapters/shared/reap-checkout-metros.sh +9 -0
- 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/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +12 -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/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -0
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/overlay.js +55 -0
- package/dist/adapters/extension/readiness.js +32 -0
- package/dist/adapters/extension/reload.js +40 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +31 -3
- package/dist/adapters/harness-freshness.js +4 -2
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/devices.js +21 -0
- package/dist/adapters/mobile/doctor-checks.js +40 -0
- package/dist/adapters/mobile/fixtures.js +141 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/overlay.js +27 -0
- package/dist/adapters/mobile/prepare-target.js +38 -0
- package/dist/adapters/mobile/readiness.js +39 -0
- package/dist/adapters/mobile/reload.js +15 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +39 -3
- 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 +1 -1
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/doctor-checks.js +22 -0
- package/dist/adapters/terminal/overlay.js +18 -0
- package/dist/adapters/terminal/readiness.js +11 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +21 -3
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +3 -1
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/debug.js +6 -25
- package/dist/commands/device-target.js +2 -1
- package/dist/commands/doctor.js +26 -137
- package/dist/commands/fixtures.js +13 -217
- package/dist/commands/launch/device.js +2 -6
- package/dist/commands/launch/extension.js +3 -303
- package/dist/commands/launch/index.js +1 -1
- package/dist/commands/launch/mobile.js +2 -190
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/parse-args.js +9 -4
- package/dist/commands/pin.js +226 -0
- package/dist/commands/prepare.js +23 -53
- package/dist/commands/provision.js +3 -7
- package/dist/commands/reload.js +8 -47
- 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/runtime-decision.js +5 -27
- package/dist/commands/self-test.js +1 -3
- package/dist/commands/status.js +20 -28
- package/dist/commands/stop.js +1 -1
- package/dist/doctor.js +12 -120
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/harness.js +31 -100
- 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 +30 -4
- package/dist/network-observation.js +10 -3
- package/dist/paths.js +8 -2
- 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/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +29 -7
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
7
|
+
function extensionBrowserDoctorCheck(target) {
|
|
8
|
+
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
9
|
+
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
10
|
+
try {
|
|
11
|
+
const playwright = requireFromTarget(packageName);
|
|
12
|
+
const executable = playwright.chromium?.executablePath?.();
|
|
13
|
+
if (executable && fs.existsSync(executable)) {
|
|
14
|
+
return {
|
|
15
|
+
id: "playwright-chromium",
|
|
16
|
+
status: "pass",
|
|
17
|
+
required: true,
|
|
18
|
+
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
19
|
+
detail: executable
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
id: "playwright-chromium",
|
|
27
|
+
status: "fail",
|
|
28
|
+
required: true,
|
|
29
|
+
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
30
|
+
userAction: "yarn playwright install chromium"
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function extensionBrowserResolutionDoctorCheck(target) {
|
|
34
|
+
const resolver = path.join(runnerDir, "adapters/shared/browser-resolver.cjs");
|
|
35
|
+
const runtime = path.join(path.resolve(target), recipeRuntimeDir());
|
|
36
|
+
const record = path.join(runtime, "browser-resolution.json");
|
|
37
|
+
let profile = process.env.CHROME_USER_DATA_DIR || path.join(runtime, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
38
|
+
while (profile.length > 1 && profile.endsWith("/")) profile = profile.slice(0, -1);
|
|
39
|
+
const profileExists = fs.existsSync(path.join(profile, "Local State"));
|
|
40
|
+
const result = spawnSync(process.execPath, [
|
|
41
|
+
resolver,
|
|
42
|
+
"resolve",
|
|
43
|
+
"--target",
|
|
44
|
+
path.resolve(target),
|
|
45
|
+
"--launch-method",
|
|
46
|
+
"launch-services",
|
|
47
|
+
"--display",
|
|
48
|
+
"headful",
|
|
49
|
+
...profileExists && fs.existsSync(record) ? ["--recorded", record] : [],
|
|
50
|
+
"--json"
|
|
51
|
+
], {
|
|
52
|
+
cwd: path.resolve(target),
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: 135e3
|
|
55
|
+
});
|
|
56
|
+
if (result.status === 0) {
|
|
57
|
+
const resolution = JSON.parse(result.stdout);
|
|
58
|
+
const managed = resolution.managedChrome;
|
|
59
|
+
const managedNote = managed && !resolution.warning?.includes("managed by policy") ? ` Google Chrome is managed by policy (${managed.forcedExtensions.length} forced extension${managed.forcedExtensions.length === 1 ? "" : "s"}: ${managed.forcedExtensions.map(({ id, name }) => name ? `${id} (${name})` : id).join(", ")}${managed.signinForced ? "; browser sign-in forced" : ""}); slots use Chrome for Testing.` : "";
|
|
60
|
+
const probe = resolution.probe ? `; Chrome for Testing probe (${resolution.probe.launchMethod}, ${resolution.probe.display}${resolution.probe.cached ? ", cached" : ""}${resolution.probe.transient ? `, retried after ${resolution.probe.expiresAt}` : ""}): ${resolution.probe.reason}` : resolution.recordedFrom ? `; kept from the slot profile record ${resolution.recordedFrom}` : "";
|
|
61
|
+
return {
|
|
62
|
+
id: "extension-browser",
|
|
63
|
+
status: "pass",
|
|
64
|
+
required: false,
|
|
65
|
+
message: `Launch browser: ${resolution.browser} ${resolution.version ?? "(unknown version)"} via ${resolution.source} (${resolution.mode}); extension loading: ${resolution.extensionLoading}${probe}.${managedNote}${resolution.warning ? ` Warning: ${resolution.warning}` : ""}`,
|
|
66
|
+
detail: resolution.bin
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const message = (result.stderr || result.error?.message || "browser resolution failed").trim();
|
|
70
|
+
return {
|
|
71
|
+
id: "extension-browser",
|
|
72
|
+
status: "fail",
|
|
73
|
+
required: message.includes("BROWSER_UNLAUNCHABLE:"),
|
|
74
|
+
message,
|
|
75
|
+
userAction: "install Google Chrome, or set RECIPE_HARNESS_BROWSER / RECIPE_HARNESS_CHROME_BIN to a browser that starts"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function ensureExtensionPerpsBuildFlag(target) {
|
|
79
|
+
const configPath = path.join(target, ".metamaskrc");
|
|
80
|
+
const templatePath = path.join(target, ".metamaskrc.dist");
|
|
81
|
+
const config = readTextSnapshot(configPath);
|
|
82
|
+
const template = config.kind === "file" ? null : readTextSnapshot(templatePath);
|
|
83
|
+
const source = config.kind === "file" ? config : template?.kind === "file" ? template : config.kind === "symlink" ? { kind: "file", text: "", mode: 438 } : null;
|
|
84
|
+
if (!source) return false;
|
|
85
|
+
const before = source.text;
|
|
86
|
+
if (/^\s*PERPS_ENABLED\s*=\s*(['"]?)true\1\s*$/mu.test(before)) return false;
|
|
87
|
+
const line = "PERPS_ENABLED='true'";
|
|
88
|
+
const after = /^\s*PERPS_ENABLED\s*=.*$/mu.test(before) ? before.replace(/^\s*PERPS_ENABLED\s*=.*$/mu, line) : `${before.replace(/\s*$/u, "")}
|
|
89
|
+
${line}
|
|
90
|
+
`;
|
|
91
|
+
const temporary = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
92
|
+
let descriptor = null;
|
|
93
|
+
try {
|
|
94
|
+
const mode = config.kind === "file" ? config.mode : 438;
|
|
95
|
+
descriptor = fs.openSync(
|
|
96
|
+
temporary,
|
|
97
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
98
|
+
mode
|
|
99
|
+
);
|
|
100
|
+
fs.writeFileSync(descriptor, after);
|
|
101
|
+
if (config.kind === "file") fs.fchmodSync(descriptor, config.mode);
|
|
102
|
+
fs.closeSync(descriptor);
|
|
103
|
+
descriptor = null;
|
|
104
|
+
fs.renameSync(temporary, configPath);
|
|
105
|
+
} finally {
|
|
106
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
107
|
+
fs.rmSync(temporary, { force: true });
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
function readTextSnapshot(file) {
|
|
112
|
+
let descriptor;
|
|
113
|
+
try {
|
|
114
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const code = error.code;
|
|
117
|
+
if (code === "ENOENT") return { kind: "missing" };
|
|
118
|
+
if (code === "ELOOP") return { kind: "symlink" };
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return {
|
|
123
|
+
kind: "file",
|
|
124
|
+
text: fs.readFileSync(descriptor, "utf8"),
|
|
125
|
+
mode: fs.fstatSync(descriptor).mode & 511
|
|
126
|
+
};
|
|
127
|
+
} finally {
|
|
128
|
+
fs.closeSync(descriptor);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
ensureExtensionPerpsBuildFlag,
|
|
133
|
+
extensionBrowserDoctorCheck,
|
|
134
|
+
extensionBrowserResolutionDoctorCheck
|
|
135
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { colorHumanMessage } from "../../cli-color.js";
|
|
4
|
+
import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
|
|
5
|
+
import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
|
|
6
|
+
import { sameFeatureFlags } from "../../feature-flags.js";
|
|
7
|
+
import { runnerDir } from "../../paths.js";
|
|
8
|
+
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
9
|
+
async function applyExtensionFixture({
|
|
10
|
+
target,
|
|
11
|
+
fixturePath,
|
|
12
|
+
canonicalFixture,
|
|
13
|
+
json,
|
|
14
|
+
sub: action,
|
|
15
|
+
fixtureFlags
|
|
16
|
+
}) {
|
|
17
|
+
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
18
|
+
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
19
|
+
if (!fs.existsSync(distManifest)) {
|
|
20
|
+
const message2 = `Extension fixture needs an existing dist/chrome build; no manifest was found at ${distManifest}.`;
|
|
21
|
+
if (json) {
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
schemaVersion: 1,
|
|
24
|
+
command: "fixtures",
|
|
25
|
+
action,
|
|
26
|
+
adapter: "extension",
|
|
27
|
+
fixture: fixturePath,
|
|
28
|
+
canonicalFixture,
|
|
29
|
+
status: "fail",
|
|
30
|
+
exitCode: EXIT.runtime,
|
|
31
|
+
error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
|
|
32
|
+
}, null, 2));
|
|
33
|
+
} else {
|
|
34
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
|
|
35
|
+
Next: ${userAction}`);
|
|
36
|
+
}
|
|
37
|
+
return EXIT.runtime;
|
|
38
|
+
}
|
|
39
|
+
process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
|
|
40
|
+
`);
|
|
41
|
+
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
42
|
+
const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
|
|
43
|
+
const flagKeys = Object.keys(fixtureFlags);
|
|
44
|
+
if (flagKeys.length > 0) args.push("--remote-flags-json", JSON.stringify(fixtureFlags));
|
|
45
|
+
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
46
|
+
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
47
|
+
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
48
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
49
|
+
});
|
|
50
|
+
const pinned = readExtensionPinnedFlags(target);
|
|
51
|
+
const pinnedFlags = pinned.pinned ?? {};
|
|
52
|
+
const flagMismatch = result.status === 0 && !pinned.error && (pinned.pinned === null || !sameFeatureFlags(pinnedFlags, fixtureFlags)) ? [.../* @__PURE__ */ new Set([...flagKeys, ...Object.keys(pinnedFlags)])].filter(
|
|
53
|
+
(key) => JSON.stringify(pinnedFlags[key]) !== JSON.stringify(fixtureFlags[key])
|
|
54
|
+
) : [];
|
|
55
|
+
if (result.status === 0 && (pinned.error || flagMismatch.length > 0)) {
|
|
56
|
+
const detail = pinned.error ? `the launched snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}` : `snapshot pin disagrees with the fixture for: ${flagMismatch.join(", ")}`;
|
|
57
|
+
const flagNext = `mm-harness launch --adapter extension --build --target ${JSON.stringify(target)}`;
|
|
58
|
+
if (json) {
|
|
59
|
+
console.log(JSON.stringify({
|
|
60
|
+
schemaVersion: 1,
|
|
61
|
+
command: "fixtures",
|
|
62
|
+
action,
|
|
63
|
+
adapter: "extension",
|
|
64
|
+
fixture: fixturePath,
|
|
65
|
+
canonicalFixture,
|
|
66
|
+
status: "fail",
|
|
67
|
+
exitCode: EXIT.runtime,
|
|
68
|
+
error: { code: "EXTENSION_FIXTURE_FLAGS_NOT_APPLIED", message: detail, userAction: flagNext }
|
|
69
|
+
}, null, 2));
|
|
70
|
+
} else {
|
|
71
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${detail}
|
|
72
|
+
Next: ${flagNext}`);
|
|
73
|
+
}
|
|
74
|
+
return EXIT.runtime;
|
|
75
|
+
}
|
|
76
|
+
const status = result.status === 0 ? "pass" : "fail";
|
|
77
|
+
const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
78
|
+
const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
|
|
79
|
+
if (json) {
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
command: "fixtures",
|
|
83
|
+
action,
|
|
84
|
+
adapter: "extension",
|
|
85
|
+
fixture: fixturePath,
|
|
86
|
+
canonicalFixture,
|
|
87
|
+
status,
|
|
88
|
+
exitCode,
|
|
89
|
+
profileReset: status === "pass",
|
|
90
|
+
message,
|
|
91
|
+
error: status === "fail" ? { code: "EXTENSION_FIXTURE_APPLY_FAILED", message, userAction } : null
|
|
92
|
+
}, null, 2));
|
|
93
|
+
} else if (status === "pass") {
|
|
94
|
+
console.error(message);
|
|
95
|
+
} else {
|
|
96
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message}
|
|
97
|
+
Next: ${userAction}`);
|
|
98
|
+
}
|
|
99
|
+
return exitCode;
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
applyExtensionFixture
|
|
103
|
+
};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { colorHumanMessage } from "../../cli-color.js";
|
|
3
|
+
import {
|
|
4
|
+
extensionDepsBlock,
|
|
5
|
+
extensionRuntimeReusable,
|
|
6
|
+
installExtensionDeps,
|
|
7
|
+
launchExtension
|
|
8
|
+
} from "../../commands/launch/extension.js";
|
|
9
|
+
import { shellQuote } from "../../commands/launch/quote.js";
|
|
10
|
+
import { EXIT, spawnScript } from "../../commands/shared.js";
|
|
11
|
+
import {
|
|
12
|
+
fixtureFeatureFlags,
|
|
13
|
+
sameFeatureFlags,
|
|
14
|
+
toRemoteFlagSpec
|
|
15
|
+
} from "../../feature-flags.js";
|
|
16
|
+
import { RECOVERY_CODE } from "../../heal-bounds.js";
|
|
17
|
+
import { runnerDir, walletFixturePath } from "../../paths.js";
|
|
18
|
+
import { ensureExtensionConsoleCapture } from "./console-capture.js";
|
|
19
|
+
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
20
|
+
import { extensionProductConfigBlock } from "./product-config.js";
|
|
21
|
+
import { releaseArtifactState, runtimeDistCheck } from "./runtime-decision.js";
|
|
22
|
+
const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
|
|
23
|
+
function verifyExtensionPin(target, requested) {
|
|
24
|
+
const pinned = readExtensionPinnedFlags(target);
|
|
25
|
+
if (pinned.error) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
flags: {},
|
|
29
|
+
code: "RUNTIME_MANIFEST_UNREADABLE",
|
|
30
|
+
message: `the launched dist snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (pinned.overrides !== null && pinned.pinned === null) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
flags: {},
|
|
37
|
+
code: "REMOTE_FLAG_NOT_APPLIED",
|
|
38
|
+
message: `the launched dist snapshot manifest (${pinned.sourcePath ?? "unknown path"}) carries no harness pin record; it was not written by this launch.`
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const actual = pinned.pinned ?? {};
|
|
42
|
+
if (!sameFeatureFlags(actual, requested)) {
|
|
43
|
+
const mismatched = [.../* @__PURE__ */ new Set([...Object.keys(requested), ...Object.keys(actual)])].filter(
|
|
44
|
+
(key) => JSON.stringify(actual[key]) !== JSON.stringify(requested[key])
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
flags: {},
|
|
49
|
+
code: "REMOTE_FLAG_NOT_APPLIED",
|
|
50
|
+
message: `the remote feature-flag pin ${toRemoteFlagSpec(requested) || "(none)"} does not match the launched snapshot manifest: ` + mismatched.map((key) => `${key} expected ${JSON.stringify(requested[key])}, got ${JSON.stringify(actual[key])}`).join("; ")
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, flags: actual, message: "" };
|
|
54
|
+
}
|
|
55
|
+
function extensionQuickReattachFailed(output) {
|
|
56
|
+
return /\[reattach\]|extension quick reattach|mm-harness launch --build/u.test(output);
|
|
57
|
+
}
|
|
58
|
+
function extensionRuntimeBlocked(output) {
|
|
59
|
+
return /ERR_BLOCKED_BY_CLIENT|has been blocked by Chromium/u.test(output);
|
|
60
|
+
}
|
|
61
|
+
function extensionBrowserMissing(output) {
|
|
62
|
+
return /Playwright Chromium is not installed|No compatible Chromium binary selected/u.test(output);
|
|
63
|
+
}
|
|
64
|
+
function extensionProductConfigMissing(output) {
|
|
65
|
+
return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
|
|
66
|
+
}
|
|
67
|
+
function extensionEvmRpcUnreachable(output) {
|
|
68
|
+
return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
|
|
69
|
+
}
|
|
70
|
+
const extensionLaunchProfile = {
|
|
71
|
+
refuseFlags({ target }, { watch, runway, clearMetro, remoteFlagSpec }) {
|
|
72
|
+
if (runway) return { message: "runway is mobile-only.", userAction: "drop --runway for the extension" };
|
|
73
|
+
if (clearMetro) return { message: "--clear-metro is mobile-only.", userAction: "drop --clear-metro for the extension" };
|
|
74
|
+
if (remoteFlagSpec !== void 0 && watch) {
|
|
75
|
+
return {
|
|
76
|
+
message: "--remote-flag cannot be applied with --watch: the watcher never re-creates the dist snapshot that carries the pin.",
|
|
77
|
+
userAction: `mm-harness launch --adapter extension --remote-flag ${remoteFlagSpec} --target ${shellQuote(target)}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return void 0;
|
|
81
|
+
},
|
|
82
|
+
// --url: initial main-tab page for extension launches (dapp beside the
|
|
83
|
+
// sidepanel); travels to live.sh/launch-browser via env. Sidepanel mode has a
|
|
84
|
+
// useful default so `mm-harness launch --sidepanel` opens a working dapp, not
|
|
85
|
+
// just a wallet panel with no page to connect from.
|
|
86
|
+
start({ options, displayMode, machine, wantWatch, tier }) {
|
|
87
|
+
const requestedUrl = typeof options["url"] === "string" && options["url"] ? options["url"] : "";
|
|
88
|
+
if (requestedUrl || displayMode === "sidepanel") {
|
|
89
|
+
process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
|
|
90
|
+
}
|
|
91
|
+
if (machine) return;
|
|
92
|
+
const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
|
|
93
|
+
const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
|
|
94
|
+
console.error(colorHumanMessage(
|
|
95
|
+
`\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
|
|
96
|
+
));
|
|
97
|
+
},
|
|
98
|
+
nativeInputs: ["package.json", "yarn.lock", "webpack.config.js"],
|
|
99
|
+
rebuildCommand: () => "mm-harness launch --build",
|
|
100
|
+
// Source-development launch paths require checkout tooling. A verified local
|
|
101
|
+
// release artifact can reuse its healthy browser without building or watching.
|
|
102
|
+
async healthcheck(launch, state) {
|
|
103
|
+
const { target, tier, wantWatch, heal, stream } = launch;
|
|
104
|
+
const artifact = releaseArtifactState(target);
|
|
105
|
+
const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
|
|
106
|
+
const verifiedReleaseReuse = artifact.status === "valid" && reusable;
|
|
107
|
+
const verifiedReleaseLaunch = !wantWatch && tier !== "build" && artifact.status === "valid" && runtimeDistCheck(target, artifact).status === "fresh";
|
|
108
|
+
const willBuild = !reusable;
|
|
109
|
+
if (willBuild) {
|
|
110
|
+
const productConfigBlock = extensionProductConfigBlock(target);
|
|
111
|
+
if (productConfigBlock) {
|
|
112
|
+
return launch.fail(state, {
|
|
113
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
114
|
+
message: productConfigBlock.message,
|
|
115
|
+
recoverable: false,
|
|
116
|
+
userAction: productConfigBlock.userAction,
|
|
117
|
+
exitCode: EXIT.infra
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const depsBlock = verifiedReleaseReuse || verifiedReleaseLaunch ? null : extensionDepsBlock(target);
|
|
122
|
+
if (depsBlock) {
|
|
123
|
+
if (heal === "off") return launch.usage(depsBlock.message, depsBlock.userAction);
|
|
124
|
+
const recoveryCode = "deps.installed";
|
|
125
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
126
|
+
stream.phase("recover");
|
|
127
|
+
const repaired = await installExtensionDeps(target);
|
|
128
|
+
if (repaired.status !== 0 || extensionDepsBlock(target)) {
|
|
129
|
+
return launch.fail(state, {
|
|
130
|
+
code: "DEPENDENCY_INSTALL_FAILED",
|
|
131
|
+
message: depsBlock.message,
|
|
132
|
+
recoverable: false,
|
|
133
|
+
userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
|
|
134
|
+
originalError: repaired.output,
|
|
135
|
+
exitCode: EXIT.infra
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
state.recovered.push(recoveryCode);
|
|
139
|
+
state.mutations.push({ type: "dependencies", action: "installed", path: target });
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
},
|
|
143
|
+
compose: ({ target, wantWatch, displayMode, remoteFlagRequest }, tier) => launchExtension(target, tier, wantWatch, displayMode, remoteFlagRequest),
|
|
144
|
+
classify({ target, remoteFlagRequest }, output) {
|
|
145
|
+
if (output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
|
|
146
|
+
return {
|
|
147
|
+
code: "REMOTE_FLAG_NOT_PINNABLE",
|
|
148
|
+
message: `a verified release artifact cannot carry a remote feature-flag pin (${remoteFlagRequest?.spec ?? ""}); its manifest is signed evidence and is never rewritten. Validate the artifact as shipped (drop --remote-flag and the fixture's remoteFeatureFlags) or validate the variant on a source build.`,
|
|
149
|
+
recoverable: false,
|
|
150
|
+
// The next step must leave the artifact alone: a --build here would replace
|
|
151
|
+
// the artifact under validation with a source build.
|
|
152
|
+
userAction: Object.keys(fixtureFeatureFlags(walletFixturePath(target))).length > 0 ? `remove remoteFeatureFlags from ${shellQuote(walletFixturePath(target))}, then: mm-harness launch --adapter extension --target ${shellQuote(target)}` : `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
153
|
+
exitCode: EXIT.infra,
|
|
154
|
+
originalError: output.trim() || void 0
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (extensionProductConfigMissing(output)) {
|
|
158
|
+
const block = extensionProductConfigBlock(target);
|
|
159
|
+
return {
|
|
160
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
161
|
+
message: block?.message ?? "Extension product configuration was not ready when the build started.",
|
|
162
|
+
recoverable: false,
|
|
163
|
+
userAction: block?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
164
|
+
exitCode: EXIT.infra
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (extensionBrowserMissing(output)) {
|
|
168
|
+
return {
|
|
169
|
+
code: "EXTENSION_BROWSER_REQUIRED",
|
|
170
|
+
message: "the checkout Playwright browser is not installed; system Chrome is not a compatible unpacked-extension fallback. Installing the browser cache requires user approval.",
|
|
171
|
+
recoverable: false,
|
|
172
|
+
userAction: `ask the user for approval, then run: cd ${shellQuote(target)} && yarn playwright install chromium`,
|
|
173
|
+
exitCode: EXIT.infra
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || output.includes("EXTENSION_STARTUP_UNVERIFIED:")) {
|
|
177
|
+
return {
|
|
178
|
+
code: output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
|
|
179
|
+
message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
|
|
180
|
+
recoverable: false,
|
|
181
|
+
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
182
|
+
exitCode: EXIT.runtime,
|
|
183
|
+
originalError: output.trim() || void 0
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (output.includes("EVM_RPC_EXECUTION_FAILED:")) {
|
|
187
|
+
return {
|
|
188
|
+
code: "EVM_RPC_EXECUTION_FAILED",
|
|
189
|
+
message: "Background RPC fetch failed during JavaScript execution; this does not establish an Infura authentication or connectivity failure.",
|
|
190
|
+
recoverable: false,
|
|
191
|
+
userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
|
|
192
|
+
exitCode: EXIT.runtime,
|
|
193
|
+
originalError: output.trim() || void 0
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (extensionEvmRpcUnreachable(output)) {
|
|
197
|
+
return {
|
|
198
|
+
code: "EVM_RPC_UNREACHABLE",
|
|
199
|
+
message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
|
|
200
|
+
recoverable: false,
|
|
201
|
+
userAction: `Check the effective INFURA_PROJECT_ID from the environment, .metamaskprodrc, or ${shellQuote(path.join(target, ".metamaskrc"))}, then rerun: mm-harness launch --verify --target ${shellQuote(target)}`,
|
|
202
|
+
exitCode: EXIT.runtime,
|
|
203
|
+
originalError: output.trim() || void 0
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return void 0;
|
|
207
|
+
},
|
|
208
|
+
async beforeHeal(launch, attempt, state) {
|
|
209
|
+
if (launch.tier !== "quick" || !extensionQuickReattachFailed(attempt.output)) return void 0;
|
|
210
|
+
const recoveryCode = "chrome.relaunched";
|
|
211
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
212
|
+
launch.stream.phase("recover");
|
|
213
|
+
const rebuildAttempt = await launch.compose("build");
|
|
214
|
+
if (rebuildAttempt.status === 0) {
|
|
215
|
+
state.recovered.push(recoveryCode);
|
|
216
|
+
return await launch.settle(state, rebuildAttempt.remoteFeatureFlags);
|
|
217
|
+
}
|
|
218
|
+
return launch.fail(state, {
|
|
219
|
+
code: "EXTENSION_RELAUNCH_FAILED",
|
|
220
|
+
message: "quick reattach failed, and clean relaunch failed too.",
|
|
221
|
+
recoverable: false,
|
|
222
|
+
exitCode: EXIT.infra,
|
|
223
|
+
originalError: rebuildAttempt.output.trim() || void 0
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
recovery: () => ({ code: RECOVERY_CODE.extension, restartApp: false }),
|
|
227
|
+
// --watch only restarts the webpack watcher; the snapshot, and its pin, are
|
|
228
|
+
// untouched, so the live record is reported rather than verified against a
|
|
229
|
+
// request the watcher could never apply (--remote-flag is refused with it;
|
|
230
|
+
// the fixture baseline stays whatever the last launch pinned).
|
|
231
|
+
provenFlags({ target, wantWatch, remoteFlagRequest }, requested) {
|
|
232
|
+
if (wantWatch) return { flags: readExtensionPinnedFlags(target).pinned };
|
|
233
|
+
const pinned = verifyExtensionPin(target, requested);
|
|
234
|
+
if (pinned.ok) return { flags: pinned.flags };
|
|
235
|
+
const spec = remoteFlagRequest?.spec;
|
|
236
|
+
return {
|
|
237
|
+
code: pinned.code ?? "REMOTE_FLAG_NOT_APPLIED",
|
|
238
|
+
message: pinned.message,
|
|
239
|
+
recoverable: false,
|
|
240
|
+
// A rebuild re-creates the snapshot; the flag is repeated only when one was asked for.
|
|
241
|
+
userAction: `mm-harness launch --adapter extension --build${spec ? ` --remote-flag ${spec}` : ""} --target ${shellQuote(target)}`,
|
|
242
|
+
exitCode: EXIT.runtime
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
async finish(launch, state, sidepanelRecoveryAllowed) {
|
|
246
|
+
const { target, machine, displayMode, tier, stream } = launch;
|
|
247
|
+
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
248
|
+
if (displayMode === "fullscreen") {
|
|
249
|
+
const sidepanelArgs = ["close"];
|
|
250
|
+
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
251
|
+
const fullscreen = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
|
|
252
|
+
if (fullscreen.status !== 0) {
|
|
253
|
+
return launch.fail(state, {
|
|
254
|
+
code: "FULLSCREEN_OPEN_FAILED",
|
|
255
|
+
message: "app launched but restoring the fullscreen Extension surface failed.",
|
|
256
|
+
recoverable: false,
|
|
257
|
+
userAction: `bash adapters/extension/sidepanel-toggle.sh close --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
|
|
258
|
+
exitCode: EXIT.runtime,
|
|
259
|
+
originalError: fullscreen.output.trim() || void 0
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (displayMode === "sidepanel") {
|
|
264
|
+
const sidepanelArgs = ["open"];
|
|
265
|
+
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
266
|
+
if (!machine) {
|
|
267
|
+
console.error(
|
|
268
|
+
`[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
|
|
272
|
+
if (sidepanel.status !== 0) {
|
|
273
|
+
const recoveryCode = "chrome.relaunched";
|
|
274
|
+
if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
|
|
275
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
276
|
+
stream.phase("recover");
|
|
277
|
+
const rebuildAttempt = await launch.compose("build");
|
|
278
|
+
if (rebuildAttempt.status === 0) {
|
|
279
|
+
state.recovered.push(recoveryCode);
|
|
280
|
+
return await launch.settle(state, void 0, false);
|
|
281
|
+
}
|
|
282
|
+
return launch.fail(state, {
|
|
283
|
+
code: "EXTENSION_RELAUNCH_FAILED",
|
|
284
|
+
message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
|
|
285
|
+
recoverable: false,
|
|
286
|
+
exitCode: EXIT.infra,
|
|
287
|
+
originalError: rebuildAttempt.output.trim() || void 0
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return launch.fail(state, {
|
|
291
|
+
code: "SIDEPANEL_OPEN_FAILED",
|
|
292
|
+
message: "app launched but opening the side panel failed.",
|
|
293
|
+
recoverable: false,
|
|
294
|
+
userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
|
|
295
|
+
exitCode: EXIT.runtime,
|
|
296
|
+
originalError: sidepanel.output.trim() || void 0
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await ensureExtensionConsoleCapture(target);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (!machine) {
|
|
304
|
+
console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return void 0;
|
|
308
|
+
},
|
|
309
|
+
verifyArgs: () => process.env.CDP_PORT ? ["--cdp-port", process.env.CDP_PORT] : [],
|
|
310
|
+
platformLabel: ({ mobileTarget, displayMode }) => mobileTarget ?? displayMode,
|
|
311
|
+
deviceLabel: ({ displayMode }) => displayMode
|
|
312
|
+
};
|
|
313
|
+
export {
|
|
314
|
+
extensionLaunchProfile
|
|
315
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isRecord } from "../../commands/parse-args.js";
|
|
5
|
+
import { recipeRuntimeDir } from "../../paths.js";
|
|
6
|
+
function readRecord(file) {
|
|
7
|
+
try {
|
|
8
|
+
const recorded = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
9
|
+
return isRecord(recorded) ? recorded : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const browserCdp = createRequire(import.meta.url)("../../../adapters/shared/browser-cdp.cjs");
|
|
15
|
+
function verifyLaunch(record, cdpPort) {
|
|
16
|
+
const launch = isRecord(record.launch) ? record.launch : {};
|
|
17
|
+
const pid = Number(launch.pid);
|
|
18
|
+
const port = Number(launch.cdpPort);
|
|
19
|
+
if (!Number.isInteger(pid) || !Number.isInteger(port) || typeof launch.profile !== "string" || typeof launch.processStartedAt !== "string") {
|
|
20
|
+
return "the browser record has no process identity (pid, start time, profile, CDP port)";
|
|
21
|
+
}
|
|
22
|
+
if (cdpPort && port !== Number(cdpPort)) return `the browser record is for CDP port ${port}, not ${cdpPort}`;
|
|
23
|
+
try {
|
|
24
|
+
const owner = browserCdp.cdpOwner(port, launch.profile);
|
|
25
|
+
if (owner.pid !== pid || owner.startedAt !== launch.processStartedAt) {
|
|
26
|
+
return `CDP port ${port} is now held by another browser process than the recorded pid ${pid}`;
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return error instanceof Error ? error.message : String(error);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function launchedBrowser(target, artifactsDir, cdpPort) {
|
|
34
|
+
const runtime = readRecord(path.join(artifactsDir, "extension-runtime", "runtime.json"));
|
|
35
|
+
const autolaunched = runtime && isRecord(runtime.browser) ? runtime.browser : null;
|
|
36
|
+
const candidates = [];
|
|
37
|
+
if (autolaunched) candidates.push([autolaunched, "run-autolaunch"]);
|
|
38
|
+
const slot = readRecord(path.join(path.resolve(target), recipeRuntimeDir(), "browser-resolution.json"));
|
|
39
|
+
if (slot) candidates.push([slot, "slot-runtime"]);
|
|
40
|
+
if (candidates.length === 0) return null;
|
|
41
|
+
const reasons = [];
|
|
42
|
+
for (const [record, boundTo] of candidates) {
|
|
43
|
+
const problem = verifyLaunch(record, cdpPort);
|
|
44
|
+
if (problem === null) return { ...record, boundTo };
|
|
45
|
+
reasons.push(`${boundTo}: ${problem}`);
|
|
46
|
+
}
|
|
47
|
+
return { boundTo: "none", reason: reasons.join("; ") };
|
|
48
|
+
}
|
|
49
|
+
export {
|
|
50
|
+
launchedBrowser
|
|
51
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { readRuntimeContextField, resolveRuntimeContextPath } from "../../harness.js";
|
|
3
|
+
import { resolveExtensionSlotPorts } from "../slot-ports.js";
|
|
4
|
+
function hasArg(args, needle) {
|
|
5
|
+
return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
|
|
6
|
+
}
|
|
7
|
+
function applyExtensionRuntimeEnv(target, args) {
|
|
8
|
+
const contextPath = resolveRuntimeContextPath(target);
|
|
9
|
+
const contextExists = fs.existsSync(contextPath);
|
|
10
|
+
if (contextExists) {
|
|
11
|
+
process.env.RECIPE_RUNTIME_CONTEXT = contextPath;
|
|
12
|
+
if (!process.env.RECIPE_SLOT_ID) {
|
|
13
|
+
const slotId = readRuntimeContextField(contextPath, "slotId");
|
|
14
|
+
if (slotId) process.env.RECIPE_SLOT_ID = slotId;
|
|
15
|
+
}
|
|
16
|
+
if (!process.env.RECIPE_HARNESS_EXTENSION_ID) {
|
|
17
|
+
const extensionId = readRuntimeContextField(contextPath, "extensionId");
|
|
18
|
+
if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
delete process.env.RECIPE_RUNTIME_CONTEXT;
|
|
22
|
+
}
|
|
23
|
+
resolveExtensionSlotPorts(target);
|
|
24
|
+
let result = [...args];
|
|
25
|
+
if (!hasArg(result, "--cdp-port")) {
|
|
26
|
+
const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
27
|
+
if (cdpPort) {
|
|
28
|
+
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
29
|
+
process.env.CDP_PORT = cdpPort;
|
|
30
|
+
result = [...result, "--cdp-port", cdpPort];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
const extensionHarness = {
|
|
36
|
+
install: { entry: "adapters/extension/inject.mjs", fallback: "scripts/inject-extension-harness.mjs", node: true },
|
|
37
|
+
cleanup: { entry: "adapters/extension/cleanup.mjs", fallback: "scripts/cleanup-extension-harness.mjs", node: true, fromInstalledRunner: true },
|
|
38
|
+
// Runner-owned typed code, not a shipped leaf: no installed overlay copy can shadow it.
|
|
39
|
+
verify: () => ({ inProcess: async () => (await import("./verify.js")).runExtensionVerify }),
|
|
40
|
+
verifyArgs: applyExtensionRuntimeEnv,
|
|
41
|
+
restart: "mm-harness launch"
|
|
42
|
+
};
|
|
43
|
+
const extensionRuntimeContext = {
|
|
44
|
+
forbiddenFields: ["metroPort", "simulator", "simulatorUdid", "adbSerial"],
|
|
45
|
+
localPorts: { cdp: 2e4, watcher: 3e4 },
|
|
46
|
+
resources: ({ existing, defaults, envPort }) => ({
|
|
47
|
+
cdpPort: envPort("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort
|
|
48
|
+
})
|
|
49
|
+
};
|
|
50
|
+
const extensionPrepare = {};
|
|
51
|
+
export {
|
|
52
|
+
extensionHarness,
|
|
53
|
+
extensionPrepare,
|
|
54
|
+
extensionRuntimeContext
|
|
55
|
+
};
|