@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
package/dist/commands/status.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
|
|
4
3
|
import { featureFlagReport } from "../feature-flags.js";
|
|
5
4
|
import { color } from "../cli-color.js";
|
|
6
5
|
import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
|
|
@@ -10,15 +9,6 @@ import { getAdapterSurface } from "../adapters/surface.js";
|
|
|
10
9
|
import {
|
|
11
10
|
applyDeviceTargeting
|
|
12
11
|
} from "./device-target.js";
|
|
13
|
-
import {
|
|
14
|
-
mobileDeviceView,
|
|
15
|
-
mobileDeviceLiveView,
|
|
16
|
-
nextForLive,
|
|
17
|
-
renderAndroidPortHints,
|
|
18
|
-
renderMobileDeviceList,
|
|
19
|
-
renderAdditionalReachableDevices,
|
|
20
|
-
renderMobileLiveBlock
|
|
21
|
-
} from "./mobile-device-view.js";
|
|
22
12
|
import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
|
|
23
13
|
import { handleStatusTaskView, renderAcceptanceLines } from "./status-watch.js";
|
|
24
14
|
import { collectTaskView, findTaskDir } from "../task-view.js";
|
|
@@ -45,23 +35,25 @@ async function handleStatus({ options }) {
|
|
|
45
35
|
const view = collectCheckoutView(target, adapter);
|
|
46
36
|
const surface = getAdapterSurface(adapter);
|
|
47
37
|
surface.resolveSlotPorts(target);
|
|
48
|
-
|
|
38
|
+
const { readiness } = surface;
|
|
39
|
+
const deviceSource = await readiness.devices?.();
|
|
40
|
+
if (deviceSource) {
|
|
49
41
|
const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
|
|
50
42
|
if ("code" in dtResult) {
|
|
51
43
|
return usageOut(json, "status", dtResult.message, dtResult.userAction);
|
|
52
44
|
}
|
|
53
45
|
}
|
|
54
46
|
const next = surface.hints.relaunch;
|
|
55
|
-
const
|
|
56
|
-
const staticFeatureFlags =
|
|
57
|
-
const deviceView =
|
|
47
|
+
const pinnedFlags = readiness.pinnedFlags?.(target) ?? null;
|
|
48
|
+
const staticFeatureFlags = pinnedFlags ? featureFlagReport(pinnedFlags.overrides, pinnedFlags) : featureFlagReport(null);
|
|
49
|
+
const deviceView = deviceSource ? deviceSource.view(allDevices) : null;
|
|
58
50
|
const devices = deviceView?.devices ?? [];
|
|
59
51
|
const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
|
|
60
52
|
const discoveryNext = discovery.deviceDiscoveryErrors?.[0]?.userAction;
|
|
61
53
|
if (json) {
|
|
62
|
-
const doProbe = !fast &&
|
|
54
|
+
const doProbe = !fast && deviceSource && deviceView !== null && deviceView.allConnectedDevices.length > 0;
|
|
63
55
|
if (doProbe) {
|
|
64
|
-
const liveView = await
|
|
56
|
+
const liveView = await deviceSource.live(target, deviceView);
|
|
65
57
|
console.log(
|
|
66
58
|
JSON.stringify(
|
|
67
59
|
{
|
|
@@ -75,14 +67,14 @@ async function handleStatus({ options }) {
|
|
|
75
67
|
...discovery,
|
|
76
68
|
...liveView.additionalReachableDevices.length > 0 ? { additionalReachableDevices: liveView.additionalReachableDevices } : {},
|
|
77
69
|
...liveView.androidPortHints.length > 0 ? { androidPortHints: liveView.androidPortHints } : {},
|
|
78
|
-
next: discoveryNext ?? nextForLive(next, liveView.liveMap)
|
|
70
|
+
next: discoveryNext ?? deviceSource.nextForLive(next, liveView.liveMap)
|
|
79
71
|
},
|
|
80
72
|
null,
|
|
81
73
|
2
|
|
82
74
|
)
|
|
83
75
|
);
|
|
84
76
|
} else {
|
|
85
|
-
const runtime = !fast &&
|
|
77
|
+
const runtime = !fast && readiness.statusRuntime ? await surface.runtimeStatus(target) : void 0;
|
|
86
78
|
console.log(
|
|
87
79
|
JSON.stringify(
|
|
88
80
|
{
|
|
@@ -113,21 +105,21 @@ async function handleStatus({ options }) {
|
|
|
113
105
|
console.log(renderAcceptanceLines(taskView).join("\n"));
|
|
114
106
|
}
|
|
115
107
|
}
|
|
116
|
-
if (
|
|
117
|
-
if (deviceView)
|
|
118
|
-
if (!fast &&
|
|
119
|
-
const liveView = await
|
|
120
|
-
if (liveView.additionalReachableDevices.length > 0)
|
|
108
|
+
if (!deviceSource) renderFeatureFlagLine(staticFeatureFlags, out);
|
|
109
|
+
if (deviceSource && deviceView) deviceSource.renderList(deviceView, out);
|
|
110
|
+
if (!fast && deviceSource && deviceView !== null && deviceView.allConnectedDevices.length > 0) {
|
|
111
|
+
const liveView = await deviceSource.live(target, deviceView);
|
|
112
|
+
if (liveView.additionalReachableDevices.length > 0) deviceSource.renderAdditional(liveView.additionalReachableDevices, out);
|
|
121
113
|
renderFeatureFlagLine(featureFlagReport(liveView.featureFlagOverrides), out);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? nextForLive(next, liveView.liveMap))}`);
|
|
125
|
-
} else if (!fast &&
|
|
114
|
+
deviceSource.renderLive(devices, liveView.liveMap, out);
|
|
115
|
+
deviceSource.renderPortHints(liveView.androidPortHints, out);
|
|
116
|
+
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? deviceSource.nextForLive(next, liveView.liveMap))}`);
|
|
117
|
+
} else if (!fast && readiness.statusRuntime) {
|
|
126
118
|
const runtime = await surface.runtimeStatus(target);
|
|
127
119
|
renderRuntimeStatus(runtime, out);
|
|
128
120
|
console.log(`${out("label", "Next:")} ${out("cmd", nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime))}`);
|
|
129
121
|
} else {
|
|
130
|
-
if (
|
|
122
|
+
if (deviceSource) renderFeatureFlagLine(featureFlagReport(null), out);
|
|
131
123
|
console.log(`${out("label", "Next:")} ${out("cmd", discoveryNext ?? next)}`);
|
|
132
124
|
}
|
|
133
125
|
return EXIT.ok;
|
package/dist/commands/stop.js
CHANGED
|
@@ -27,7 +27,7 @@ async function handleStop(argv) {
|
|
|
27
27
|
const collector = await stopProjectCollector(target);
|
|
28
28
|
const signalled = (stop.signalled ?? 0) + (collector.stopped ? 1 : 0);
|
|
29
29
|
const summary = collector.stopped ? `${stop.summary}; stopped analytics collector ${collector.pid}` : stop.summary;
|
|
30
|
-
const next =
|
|
30
|
+
const next = surface.devServer.afterStop?.(target);
|
|
31
31
|
const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
|
|
32
32
|
if (json) {
|
|
33
33
|
console.log(
|
package/dist/doctor.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import fs from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { color } from "./cli-color.js";
|
|
6
4
|
import { normalizeFeatureFlagRecord } from "./feature-flags.js";
|
|
7
|
-
import {
|
|
8
|
-
mobileToolRecovery,
|
|
9
|
-
resolveMobileToolPath
|
|
10
|
-
} from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
11
|
-
import { mobilePerpsEnvironment } from "./adapters/mobile/perps-env.js";
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
12
6
|
import { readRuntimeContextField, resolveRuntimeContextPath } from "./harness.js";
|
|
13
|
-
import {
|
|
7
|
+
import { isPin } from "./harness-pin.js";
|
|
8
|
+
import { harnessExecutable, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
14
9
|
function requiredDoctorCheckSummary(checks) {
|
|
15
10
|
const required = checks.filter((check) => check.required);
|
|
16
11
|
const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
|
|
@@ -41,21 +36,7 @@ function repoShape(target) {
|
|
|
41
36
|
};
|
|
42
37
|
}
|
|
43
38
|
function compatibilityMode(adapter, target) {
|
|
44
|
-
|
|
45
|
-
if (adapter === "core") {
|
|
46
|
-
return fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge";
|
|
47
|
-
}
|
|
48
|
-
if (adapter === "terminal") {
|
|
49
|
-
return fs.existsSync(path.join(target, "src/features/perpetuals")) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge";
|
|
50
|
-
}
|
|
51
|
-
if (adapter === "mobile") {
|
|
52
|
-
if (shape.injectedHarness && shape.agenticService) return "runner bridge with injected app bridge";
|
|
53
|
-
if (shape.agenticService) return "runner bridge with app bridge";
|
|
54
|
-
return "runner bridge available; app bridge not installed";
|
|
55
|
-
}
|
|
56
|
-
if (!shape.extensionProject) return "unsupported/no bridge";
|
|
57
|
-
if (shape.extensionRuntime || shape.injectedHarness) return "bridge present";
|
|
58
|
-
return "bridge injectable";
|
|
39
|
+
return getAdapterSurface(adapter).readiness.compatibilityMode(target, repoShape(target));
|
|
59
40
|
}
|
|
60
41
|
function readPackageInfo(target) {
|
|
61
42
|
const packageJsonPath = path.join(target, "package.json");
|
|
@@ -96,7 +77,8 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
|
96
77
|
);
|
|
97
78
|
const flagKeys = Object.keys(remoteFeatureFlags);
|
|
98
79
|
return {
|
|
99
|
-
|
|
80
|
+
// A headless platform signs with the accounts alone; nothing unlocks a UI.
|
|
81
|
+
status: hasAccounts && (adapter !== void 0 && getAdapterSurface(adapter).headless || hasPassword) ? "ready" : "incomplete",
|
|
100
82
|
path: reportedPath,
|
|
101
83
|
accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
|
|
102
84
|
hasPassword,
|
|
@@ -154,7 +136,8 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
154
136
|
}
|
|
155
137
|
return lines.join("\n");
|
|
156
138
|
}
|
|
157
|
-
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath =
|
|
139
|
+
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = getAdapterSurface(adapter).actions.manifestPath(), platform) {
|
|
140
|
+
const readiness = getAdapterSurface(adapter).readiness;
|
|
158
141
|
const mode = compatibilityMode(adapter, target);
|
|
159
142
|
const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
|
|
160
143
|
const checks = [
|
|
@@ -170,8 +153,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
170
153
|
required: false,
|
|
171
154
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
172
155
|
},
|
|
173
|
-
...
|
|
174
|
-
...adapter === "extension" ? [extensionBrowserDoctorCheck(target), extensionBrowserResolutionDoctorCheck(target)] : []
|
|
156
|
+
...readiness.checks?.(target, platform) ?? []
|
|
175
157
|
];
|
|
176
158
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
177
159
|
return {
|
|
@@ -187,100 +169,13 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
187
169
|
compatibilityMode: mode,
|
|
188
170
|
shape: repoShape(target),
|
|
189
171
|
fixture: fixtureSummary(target, adapter),
|
|
190
|
-
environment:
|
|
172
|
+
environment: readiness.environment?.(target) ?? {},
|
|
191
173
|
runtimeContext: runtimeContextSummary(target, adapter),
|
|
192
174
|
manifestValidation: manifestValidation.summary
|
|
193
175
|
};
|
|
194
176
|
}
|
|
195
|
-
function extensionBrowserDoctorCheck(target) {
|
|
196
|
-
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
197
|
-
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
198
|
-
try {
|
|
199
|
-
const playwright = requireFromTarget(packageName);
|
|
200
|
-
const executable = playwright.chromium?.executablePath?.();
|
|
201
|
-
if (executable && fs.existsSync(executable)) {
|
|
202
|
-
return {
|
|
203
|
-
id: "playwright-chromium",
|
|
204
|
-
status: "pass",
|
|
205
|
-
required: true,
|
|
206
|
-
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
207
|
-
detail: executable
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
id: "playwright-chromium",
|
|
215
|
-
status: "fail",
|
|
216
|
-
required: true,
|
|
217
|
-
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
218
|
-
userAction: "yarn playwright install chromium"
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
function extensionBrowserResolutionDoctorCheck(target) {
|
|
222
|
-
const resolver = path.join(runnerDir, "adapters/shared/browser-resolver.cjs");
|
|
223
|
-
const runtime = path.join(path.resolve(target), recipeRuntimeDir());
|
|
224
|
-
const record = path.join(runtime, "browser-resolution.json");
|
|
225
|
-
let profile = process.env.CHROME_USER_DATA_DIR || path.join(runtime, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
226
|
-
while (profile.length > 1 && profile.endsWith("/")) profile = profile.slice(0, -1);
|
|
227
|
-
const profileExists = fs.existsSync(path.join(profile, "Local State"));
|
|
228
|
-
const result = spawnSync(process.execPath, [
|
|
229
|
-
resolver,
|
|
230
|
-
"resolve",
|
|
231
|
-
"--target",
|
|
232
|
-
path.resolve(target),
|
|
233
|
-
"--launch-method",
|
|
234
|
-
"launch-services",
|
|
235
|
-
"--display",
|
|
236
|
-
"headful",
|
|
237
|
-
...profileExists && fs.existsSync(record) ? ["--recorded", record] : [],
|
|
238
|
-
"--json"
|
|
239
|
-
], {
|
|
240
|
-
cwd: path.resolve(target),
|
|
241
|
-
encoding: "utf8",
|
|
242
|
-
timeout: 135e3
|
|
243
|
-
});
|
|
244
|
-
if (result.status === 0) {
|
|
245
|
-
const resolution = JSON.parse(result.stdout);
|
|
246
|
-
const managed = resolution.managedChrome;
|
|
247
|
-
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.` : "";
|
|
248
|
-
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}` : "";
|
|
249
|
-
return {
|
|
250
|
-
id: "extension-browser",
|
|
251
|
-
status: "pass",
|
|
252
|
-
required: false,
|
|
253
|
-
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}` : ""}`,
|
|
254
|
-
detail: resolution.bin
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
const message = (result.stderr || result.error?.message || "browser resolution failed").trim();
|
|
258
|
-
return {
|
|
259
|
-
id: "extension-browser",
|
|
260
|
-
status: "fail",
|
|
261
|
-
required: message.includes("BROWSER_UNLAUNCHABLE:"),
|
|
262
|
-
message,
|
|
263
|
-
userAction: "install Google Chrome, or set RECIPE_HARNESS_BROWSER / RECIPE_HARNESS_CHROME_BIN to a browser that starts"
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
function mobileToolDoctorChecks(platform) {
|
|
267
|
-
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
268
|
-
{ tool: "adb", platform: "Android" },
|
|
269
|
-
{ tool: "idb", platform: "iOS" }
|
|
270
|
-
];
|
|
271
|
-
return specs.map(({ tool, platform: platformName }) => {
|
|
272
|
-
const resolved = resolveMobileToolPath(tool);
|
|
273
|
-
return {
|
|
274
|
-
id: `device-tool-${tool}`,
|
|
275
|
-
status: resolved ? "pass" : "fail",
|
|
276
|
-
required: platform !== void 0,
|
|
277
|
-
message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
|
|
278
|
-
...resolved ? { detail: resolved } : {},
|
|
279
|
-
...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
177
|
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
178
|
+
if (isPin(runnerRoot)) return "pinned-install";
|
|
284
179
|
const normalizedRoot = path.normalize(runnerRoot);
|
|
285
180
|
const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
|
|
286
181
|
const globalNodeModulesSegment = `${path.sep}lib${path.sep}node_modules${path.sep}`;
|
|
@@ -315,7 +210,7 @@ function runnerProvenance(actionManifestPath) {
|
|
|
315
210
|
const executablePath = harnessExecutable();
|
|
316
211
|
const invokedPath = path.resolve(process.env.MM_HARNESS_INVOKED_AS ?? executablePath);
|
|
317
212
|
const installKind = runnerInstallKind(runnerDir, invokedPath, executablePath);
|
|
318
|
-
const packageSource = runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
|
|
213
|
+
const packageSource = isPin(runnerDir) ? "pin" : runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
|
|
319
214
|
return {
|
|
320
215
|
name: "@metamask/recipe-runner",
|
|
321
216
|
packageName,
|
|
@@ -342,11 +237,8 @@ function readJsonObject(file) {
|
|
|
342
237
|
export {
|
|
343
238
|
compatibilityMode,
|
|
344
239
|
createDoctorReport,
|
|
345
|
-
extensionBrowserDoctorCheck,
|
|
346
|
-
extensionBrowserResolutionDoctorCheck,
|
|
347
240
|
fixtureFileSummary,
|
|
348
241
|
fixtureSummary,
|
|
349
|
-
mobileToolDoctorChecks,
|
|
350
242
|
renderRuntimeContext,
|
|
351
243
|
repoShape,
|
|
352
244
|
requiredDoctorCheckSummary,
|
|
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fs, { constants as fsConstants } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
6
|
import { indexArtifactManifest } from "./artifact-files.js";
|
|
7
7
|
import { importRecipeProtocol, runnerDir } from "./paths.js";
|
|
8
8
|
const gitContexts = /* @__PURE__ */ new Map();
|
|
@@ -156,10 +156,11 @@ function sourceSnapshot(root, adapter, excludedRoots = [], includedRoots = []) {
|
|
|
156
156
|
...pathspecs
|
|
157
157
|
]);
|
|
158
158
|
const head = gitBuffer(git.topLevel, ["rev-parse", "HEAD"]);
|
|
159
|
+
const platformFingerprint = adapter ? getAdapterSurface(adapter).sourceFingerprint : void 0;
|
|
159
160
|
return {
|
|
160
161
|
head: head.toString("utf8").trim() || null,
|
|
161
162
|
status,
|
|
162
|
-
sourceFingerprint:
|
|
163
|
+
sourceFingerprint: platformFingerprint && path.resolve(root) === git.topLevel ? platformFingerprint(root) : nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head)
|
|
163
164
|
};
|
|
164
165
|
}
|
|
165
166
|
function nestedGitSourceFingerprint(root, git, pathspecs, excludedRoots, includedRoots, head) {
|
package/dist/harness.js
CHANGED
|
@@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
|
|
6
|
-
import {
|
|
6
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
7
7
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
8
8
|
import { colorHumanMessage } from "./cli-color.js";
|
|
9
9
|
const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
|
|
@@ -37,15 +37,15 @@ Examples:
|
|
|
37
37
|
|
|
38
38
|
core supports install, verify, and cleanup (headless; no live).`);
|
|
39
39
|
}
|
|
40
|
-
function failureHint(
|
|
41
|
-
|
|
40
|
+
function failureHint(surface, action) {
|
|
41
|
+
const restart = surface.harness.restart;
|
|
42
|
+
if (!restart) {
|
|
42
43
|
if (action === "verify") {
|
|
43
|
-
return `Install the
|
|
44
|
+
return `Install the ${surface.adapter} harness first: mm-harness install --platform ${surface.adapter}`;
|
|
44
45
|
}
|
|
45
46
|
return "Read the error above for the specific cause, then re-run this command.";
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
|
|
48
|
+
return `Read the error above for the specific cause. To (re)start the runtime, run: ${restart}`;
|
|
49
49
|
}
|
|
50
50
|
function successNext(adapter, action, target) {
|
|
51
51
|
const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
@@ -67,7 +67,7 @@ function shellQuote(value) {
|
|
|
67
67
|
return /^[A-Za-z0-9_./:=@+-]+$/u.test(value) ? value : `'${value.replace(/'/gu, `'\\''`)}'`;
|
|
68
68
|
}
|
|
69
69
|
function isAdapter(value) {
|
|
70
|
-
return value
|
|
70
|
+
return value !== void 0 && adapterRegistry.has(value);
|
|
71
71
|
}
|
|
72
72
|
function parseHarnessArgs(args) {
|
|
73
73
|
const forward = [];
|
|
@@ -170,37 +170,9 @@ function readRuntimeContextField(contextPath, field) {
|
|
|
170
170
|
if (node === void 0 || node === null || node === "") return void 0;
|
|
171
171
|
return String(node);
|
|
172
172
|
}
|
|
173
|
-
function applyExtensionRuntimeEnv(target, args) {
|
|
174
|
-
const contextPath = resolveRuntimeContextPath(target);
|
|
175
|
-
const contextExists = fs.existsSync(contextPath);
|
|
176
|
-
if (contextExists) {
|
|
177
|
-
process.env.RECIPE_RUNTIME_CONTEXT = contextPath;
|
|
178
|
-
if (!process.env.RECIPE_SLOT_ID) {
|
|
179
|
-
const slotId = readRuntimeContextField(contextPath, "slotId");
|
|
180
|
-
if (slotId) process.env.RECIPE_SLOT_ID = slotId;
|
|
181
|
-
}
|
|
182
|
-
if (!process.env.RECIPE_HARNESS_EXTENSION_ID) {
|
|
183
|
-
const extensionId = readRuntimeContextField(contextPath, "extensionId");
|
|
184
|
-
if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
delete process.env.RECIPE_RUNTIME_CONTEXT;
|
|
188
|
-
}
|
|
189
|
-
resolveExtensionSlotPorts(target);
|
|
190
|
-
let result = [...args];
|
|
191
|
-
if (!hasArg(result, "--cdp-port")) {
|
|
192
|
-
const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
|
|
193
|
-
if (cdpPort) {
|
|
194
|
-
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
195
|
-
process.env.CDP_PORT = cdpPort;
|
|
196
|
-
result = [...result, "--cdp-port", cdpPort];
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return result;
|
|
200
|
-
}
|
|
201
173
|
function resolveEntry(base, candidates, mode) {
|
|
202
174
|
for (const candidate of candidates) {
|
|
203
|
-
const full = path.
|
|
175
|
+
const full = path.resolve(base, candidate);
|
|
204
176
|
try {
|
|
205
177
|
const stat = fs.statSync(full);
|
|
206
178
|
if (!stat.isFile()) continue;
|
|
@@ -208,7 +180,7 @@ function resolveEntry(base, candidates, mode) {
|
|
|
208
180
|
} catch {
|
|
209
181
|
}
|
|
210
182
|
}
|
|
211
|
-
return path.
|
|
183
|
+
return path.resolve(base, candidates[0]);
|
|
212
184
|
}
|
|
213
185
|
function installedRunnerSource(target, adapter) {
|
|
214
186
|
const pointer = path.join(recipeHarnessPath(target, adapter), "runner", ".runner-source");
|
|
@@ -217,56 +189,12 @@ function installedRunnerSource(target, adapter) {
|
|
|
217
189
|
if (!value || !fs.existsSync(value)) return void 0;
|
|
218
190
|
return value;
|
|
219
191
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
const INJECT_CANDIDATES = {
|
|
229
|
-
mobile: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
|
|
230
|
-
extension: { entry: "adapters/extension/inject.mjs", fallback: "scripts/inject-extension-harness.mjs" },
|
|
231
|
-
core: { entry: "adapters/core/inject.sh", fallback: "scripts/inject-core-harness.sh" },
|
|
232
|
-
terminal: { entry: "adapters/terminal/inject.mjs", fallback: "adapters/terminal/inject.mjs" }
|
|
233
|
-
};
|
|
234
|
-
const CLEANUP_CANDIDATES = {
|
|
235
|
-
mobile: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh" },
|
|
236
|
-
extension: { entry: "adapters/extension/cleanup.mjs", fallback: "scripts/cleanup-extension-harness.mjs" },
|
|
237
|
-
core: { entry: "adapters/core/cleanup.sh", fallback: "scripts/cleanup-core-harness.sh" },
|
|
238
|
-
terminal: { entry: "adapters/terminal/cleanup.mjs", fallback: "adapters/terminal/cleanup.mjs" }
|
|
239
|
-
};
|
|
240
|
-
function resolveHarnessDispatch(adapter, action, target) {
|
|
241
|
-
if (action === "install") {
|
|
242
|
-
const { entry, fallback } = INJECT_CANDIDATES[adapter];
|
|
243
|
-
if (adapter === "extension" || adapter === "terminal") {
|
|
244
|
-
return { command: process.execPath, prefixArgs: [resolveEntry(runnerDir, [entry, fallback], "file")] };
|
|
245
|
-
}
|
|
246
|
-
return { command: resolveEntry(runnerDir, [entry, fallback], "file"), prefixArgs: [] };
|
|
247
|
-
}
|
|
248
|
-
if (action === "cleanup") {
|
|
249
|
-
const { entry, fallback } = CLEANUP_CANDIDATES[adapter];
|
|
250
|
-
const base = adapter === "terminal" ? runnerDir : installedRunnerSource(target, adapter) ?? runnerDir;
|
|
251
|
-
if (adapter === "extension" || adapter === "terminal") {
|
|
252
|
-
return { command: process.execPath, prefixArgs: [resolveEntry(base, [entry, fallback], "file")] };
|
|
253
|
-
}
|
|
254
|
-
return { command: resolveEntry(base, [entry, fallback], "file"), prefixArgs: [] };
|
|
255
|
-
}
|
|
256
|
-
if (adapter === "terminal" && action === "verify") {
|
|
257
|
-
return { command: process.execPath, prefixArgs: [path.join(runnerDir, "adapters", "terminal", "verify.mjs")] };
|
|
258
|
-
}
|
|
259
|
-
if (adapter === "core" && action === "verify") {
|
|
260
|
-
const delegate = path.join(recipeHarnessPath(target, "core"), "runner", "bin", "mm-harness");
|
|
261
|
-
if (!isExecutable(delegate)) {
|
|
262
|
-
return {
|
|
263
|
-
error: `core recipe harness not installed: missing delegate ${delegate}.
|
|
264
|
-
Next: mm-harness install --platform core --target ${target}`
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
return { command: delegate, prefixArgs: ["doctor", "--adapter", "core"] };
|
|
268
|
-
}
|
|
269
|
-
return { inProcess: adapter === "mobile" ? "mobile-verify" : "extension-verify" };
|
|
192
|
+
function resolveHarnessDispatch(surface, action, target) {
|
|
193
|
+
if (action === "verify") return surface.harness.verify(target);
|
|
194
|
+
const leaf = action === "install" ? surface.harness.install : surface.harness.cleanup;
|
|
195
|
+
const base = action === "cleanup" && surface.harness.cleanup.fromInstalledRunner ? installedRunnerSource(target, surface.adapter) ?? runnerDir : runnerDir;
|
|
196
|
+
const entry = resolveEntry(base, [leaf.entry, leaf.fallback], "file");
|
|
197
|
+
return leaf.node ? { command: process.execPath, prefixArgs: [entry] } : { command: entry, prefixArgs: [] };
|
|
270
198
|
}
|
|
271
199
|
async function handleHarness(argv) {
|
|
272
200
|
const action = argv[0];
|
|
@@ -280,6 +208,7 @@ async function handleHarness(argv) {
|
|
|
280
208
|
return 2;
|
|
281
209
|
}
|
|
282
210
|
const harnessAction = action;
|
|
211
|
+
const { getAdapterSurface } = await import("./adapters/surface.js");
|
|
283
212
|
const { adapter: parsedAdapter, json, quiet, forward } = parseHarnessArgs(argv.slice(1));
|
|
284
213
|
const rawTarget = argValue(forward, "--target");
|
|
285
214
|
const target = path.resolve(rawTarget ?? process.cwd());
|
|
@@ -306,10 +235,11 @@ async function handleHarness(argv) {
|
|
|
306
235
|
if (harnessAction === "install" && hasArg(forward, "--runway")) {
|
|
307
236
|
return handleRunwayInstall(adapter, target, forward, json);
|
|
308
237
|
}
|
|
309
|
-
|
|
310
|
-
|
|
238
|
+
const surface = getAdapterSurface(adapter);
|
|
239
|
+
if (harnessAction === "verify" && surface.harness.verifyArgs) {
|
|
240
|
+
forwardArgs = surface.harness.verifyArgs(target, forwardArgs);
|
|
311
241
|
}
|
|
312
|
-
const dispatch = resolveHarnessDispatch(
|
|
242
|
+
const dispatch = resolveHarnessDispatch(surface, harnessAction, target);
|
|
313
243
|
if ("error" in dispatch) {
|
|
314
244
|
if (quiet) {
|
|
315
245
|
return 1;
|
|
@@ -335,7 +265,7 @@ async function handleHarness(argv) {
|
|
|
335
265
|
}
|
|
336
266
|
const start = Date.now();
|
|
337
267
|
if ("inProcess" in dispatch) {
|
|
338
|
-
const runVerify = dispatch.inProcess
|
|
268
|
+
const runVerify = await dispatch.inProcess();
|
|
339
269
|
let exitCode;
|
|
340
270
|
try {
|
|
341
271
|
exitCode = await runVerify(forwardArgs, {
|
|
@@ -355,7 +285,7 @@ async function handleHarness(argv) {
|
|
|
355
285
|
`);
|
|
356
286
|
exitCode = 1;
|
|
357
287
|
}
|
|
358
|
-
return reportHarnessOutcome(harnessAction,
|
|
288
|
+
return reportHarnessOutcome(harnessAction, surface, target, exitCode, autoDetected, json, quiet, start);
|
|
359
289
|
}
|
|
360
290
|
if (shellLeafMissing(dispatch.command)) {
|
|
361
291
|
const message = missingShellLeafMessage(dispatch.command);
|
|
@@ -366,7 +296,7 @@ async function handleHarness(argv) {
|
|
|
366
296
|
harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
|
|
367
297
|
code: "HARNESS_SPAWN_FAILED",
|
|
368
298
|
message,
|
|
369
|
-
userAction: failureHint(
|
|
299
|
+
userAction: failureHint(surface, harnessAction)
|
|
370
300
|
})
|
|
371
301
|
);
|
|
372
302
|
} else {
|
|
@@ -387,19 +317,20 @@ async function handleHarness(argv) {
|
|
|
387
317
|
harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
|
|
388
318
|
code: "HARNESS_SPAWN_FAILED",
|
|
389
319
|
message: `${harnessAction} ${adapter} could not start: ${result.error.message}`,
|
|
390
|
-
userAction: failureHint(
|
|
320
|
+
userAction: failureHint(surface, harnessAction)
|
|
391
321
|
})
|
|
392
322
|
);
|
|
393
323
|
} else {
|
|
394
324
|
console.error(`\u2717 ${harnessAction} ${adapter} could not start: ${result.error.message}
|
|
395
|
-
${failureHint(
|
|
325
|
+
${failureHint(surface, harnessAction)}`);
|
|
396
326
|
}
|
|
397
327
|
return 1;
|
|
398
328
|
}
|
|
399
|
-
return reportHarnessOutcome(harnessAction,
|
|
329
|
+
return reportHarnessOutcome(harnessAction, surface, target, result.status ?? 1, autoDetected, json, quiet, start);
|
|
400
330
|
}
|
|
401
|
-
function reportHarnessOutcome(action,
|
|
331
|
+
function reportHarnessOutcome(action, surface, target, exitCode, autoDetected, json, quiet, start) {
|
|
402
332
|
const seconds = ((Date.now() - start) / 1e3).toFixed(1);
|
|
333
|
+
const adapter = surface.adapter;
|
|
403
334
|
if (quiet) {
|
|
404
335
|
return exitCode;
|
|
405
336
|
} else if (json) {
|
|
@@ -411,7 +342,7 @@ function reportHarnessOutcome(action, adapter, target, exitCode, autoDetected, j
|
|
|
411
342
|
exitCode === 0 ? "pass" : "fail",
|
|
412
343
|
exitCode,
|
|
413
344
|
autoDetected,
|
|
414
|
-
exitCode === 0 ? void 0 : { code: "HARNESS_FAILED", message: `${action} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(
|
|
345
|
+
exitCode === 0 ? void 0 : { code: "HARNESS_FAILED", message: `${action} ${adapter} failed (exit ${exitCode})`, userAction: failureHint(surface, action) }
|
|
415
346
|
)
|
|
416
347
|
);
|
|
417
348
|
} else if (exitCode === 0) {
|
|
@@ -419,7 +350,7 @@ function reportHarnessOutcome(action, adapter, target, exitCode, autoDetected, j
|
|
|
419
350
|
Next: ${successNext(adapter, action, target)}`));
|
|
420
351
|
} else {
|
|
421
352
|
console.error(colorHumanMessage(`\u2717 ${action} ${adapter} failed (exit ${exitCode}, ${seconds}s)
|
|
422
|
-
${failureHint(
|
|
353
|
+
${failureHint(surface, action)}`));
|
|
423
354
|
}
|
|
424
355
|
return exitCode;
|
|
425
356
|
}
|
|
@@ -448,7 +379,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
|
|
|
448
379
|
});
|
|
449
380
|
const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
|
|
450
381
|
const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
|
|
451
|
-
const next = result.status === "pass" &&
|
|
382
|
+
const next = result.status === "pass" && surface.runwayProvision.launchNext && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter ${adapter} --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuote(provisionedDevice)}` : "") : void 0;
|
|
452
383
|
const output = next ? { ...result, next } : result;
|
|
453
384
|
if (json) {
|
|
454
385
|
console.log(JSON.stringify(output, null, 2));
|
package/dist/heal-bounds.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
4
5
|
import { handleHarness } from "./harness.js";
|
|
5
6
|
import { recipeHarnessPath, recipeRuntimePath } from "./paths.js";
|
|
6
7
|
import { EXIT } from "./commands/shared.js";
|
|
@@ -28,7 +29,7 @@ function overlayPresent(target, adapter) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
function overlayDelegateValid(target, adapter) {
|
|
31
|
-
if (adapter
|
|
32
|
+
if (getAdapterSurface(adapter).headless) return true;
|
|
32
33
|
const pointer = path.join(recipeHarnessPath(target, adapter), "runner", ".runner-source");
|
|
33
34
|
if (!fs.existsSync(pointer)) return true;
|
|
34
35
|
const runnerPath = fs.readFileSync(pointer, "utf8").trim();
|
|
@@ -36,7 +37,7 @@ function overlayDelegateValid(target, adapter) {
|
|
|
36
37
|
return fs.existsSync(path.join(runnerPath, "bin", "mm-harness"));
|
|
37
38
|
}
|
|
38
39
|
async function ensureOverlay(adapter, target, heal, state, json) {
|
|
39
|
-
if (adapter
|
|
40
|
+
if (getAdapterSurface(adapter).headless) return { ok: true };
|
|
40
41
|
if (overlayPresent(target, adapter) && overlayDelegateValid(target, adapter)) return { ok: true };
|
|
41
42
|
if (heal === "off") return { ok: true };
|
|
42
43
|
const installBin = process.env.MM_HARNESS_INSTALL_BIN;
|