@deeeed/metamask-harness 0.75.0 → 0.77.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 +28 -0
- package/adapters/shared/reap-checkout-metros.sh +9 -0
- package/dist/adapters/core/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/surface.js +7 -0
- package/dist/adapters/extension/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -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/surface.js +22 -1
- package/dist/adapters/harness-freshness.js +4 -2
- 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/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/surface.js +28 -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/surface.js +15 -2
- package/dist/cli.js +3 -1
- package/dist/commands/call.js +3 -7
- 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/list-executables.js +12 -22
- package/dist/commands/logs.js +1 -1
- package/dist/commands/manifest.js +11 -133
- package/dist/commands/parse-args.js +9 -4
- 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.js +2 -5
- 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 +9 -119
- package/dist/harness.js +31 -100
- package/dist/mm-harness-cli.js +3 -2
- package/dist/paths.js +8 -2
- package/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +27 -17
- package/package.json +2 -2
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,17 +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
7
|
import { isPin } from "./harness-pin.js";
|
|
14
|
-
import { harnessExecutable,
|
|
8
|
+
import { harnessExecutable, readJson, recipeHarnessRoot, recipeRuntimeDir, runnerDir } from "./paths.js";
|
|
15
9
|
function requiredDoctorCheckSummary(checks) {
|
|
16
10
|
const required = checks.filter((check) => check.required);
|
|
17
11
|
const failed = required.filter((check) => check.status === "fail").map((check) => check.id);
|
|
@@ -42,21 +36,7 @@ function repoShape(target) {
|
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
38
|
function compatibilityMode(adapter, target) {
|
|
45
|
-
|
|
46
|
-
if (adapter === "core") {
|
|
47
|
-
return fs.existsSync(path.join(target, "packages/perps-controller/src/index.ts")) ? "headless controller (no bridge)" : "unsupported/no bridge";
|
|
48
|
-
}
|
|
49
|
-
if (adapter === "terminal") {
|
|
50
|
-
return fs.existsSync(path.join(target, "src/features/perpetuals")) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge";
|
|
51
|
-
}
|
|
52
|
-
if (adapter === "mobile") {
|
|
53
|
-
if (shape.injectedHarness && shape.agenticService) return "runner bridge with injected app bridge";
|
|
54
|
-
if (shape.agenticService) return "runner bridge with app bridge";
|
|
55
|
-
return "runner bridge available; app bridge not installed";
|
|
56
|
-
}
|
|
57
|
-
if (!shape.extensionProject) return "unsupported/no bridge";
|
|
58
|
-
if (shape.extensionRuntime || shape.injectedHarness) return "bridge present";
|
|
59
|
-
return "bridge injectable";
|
|
39
|
+
return getAdapterSurface(adapter).readiness.compatibilityMode(target, repoShape(target));
|
|
60
40
|
}
|
|
61
41
|
function readPackageInfo(target) {
|
|
62
42
|
const packageJsonPath = path.join(target, "package.json");
|
|
@@ -97,7 +77,8 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
|
|
|
97
77
|
);
|
|
98
78
|
const flagKeys = Object.keys(remoteFeatureFlags);
|
|
99
79
|
return {
|
|
100
|
-
|
|
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",
|
|
101
82
|
path: reportedPath,
|
|
102
83
|
accountCount: Array.isArray(data.accounts) ? data.accounts.length : 0,
|
|
103
84
|
hasPassword,
|
|
@@ -155,7 +136,8 @@ function renderRuntimeContext(runtimeContext) {
|
|
|
155
136
|
}
|
|
156
137
|
return lines.join("\n");
|
|
157
138
|
}
|
|
158
|
-
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath =
|
|
139
|
+
function createDoctorReport(adapter, target, manifestValidation, actionManifestPath = getAdapterSurface(adapter).actions.manifestPath(), platform) {
|
|
140
|
+
const readiness = getAdapterSurface(adapter).readiness;
|
|
159
141
|
const mode = compatibilityMode(adapter, target);
|
|
160
142
|
const manifestErrors = Number(manifestValidation.summary?.errors ?? 0);
|
|
161
143
|
const checks = [
|
|
@@ -171,8 +153,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
171
153
|
required: false,
|
|
172
154
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
173
155
|
},
|
|
174
|
-
...
|
|
175
|
-
...adapter === "extension" ? [extensionBrowserDoctorCheck(target), extensionBrowserResolutionDoctorCheck(target)] : []
|
|
156
|
+
...readiness.checks?.(target, platform) ?? []
|
|
176
157
|
];
|
|
177
158
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
178
159
|
return {
|
|
@@ -188,99 +169,11 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
188
169
|
compatibilityMode: mode,
|
|
189
170
|
shape: repoShape(target),
|
|
190
171
|
fixture: fixtureSummary(target, adapter),
|
|
191
|
-
environment:
|
|
172
|
+
environment: readiness.environment?.(target) ?? {},
|
|
192
173
|
runtimeContext: runtimeContextSummary(target, adapter),
|
|
193
174
|
manifestValidation: manifestValidation.summary
|
|
194
175
|
};
|
|
195
176
|
}
|
|
196
|
-
function extensionBrowserDoctorCheck(target) {
|
|
197
|
-
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
198
|
-
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
199
|
-
try {
|
|
200
|
-
const playwright = requireFromTarget(packageName);
|
|
201
|
-
const executable = playwright.chromium?.executablePath?.();
|
|
202
|
-
if (executable && fs.existsSync(executable)) {
|
|
203
|
-
return {
|
|
204
|
-
id: "playwright-chromium",
|
|
205
|
-
status: "pass",
|
|
206
|
-
required: true,
|
|
207
|
-
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
208
|
-
detail: executable
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
id: "playwright-chromium",
|
|
216
|
-
status: "fail",
|
|
217
|
-
required: true,
|
|
218
|
-
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
219
|
-
userAction: "yarn playwright install chromium"
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function extensionBrowserResolutionDoctorCheck(target) {
|
|
223
|
-
const resolver = path.join(runnerDir, "adapters/shared/browser-resolver.cjs");
|
|
224
|
-
const runtime = path.join(path.resolve(target), recipeRuntimeDir());
|
|
225
|
-
const record = path.join(runtime, "browser-resolution.json");
|
|
226
|
-
let profile = process.env.CHROME_USER_DATA_DIR || path.join(runtime, process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile");
|
|
227
|
-
while (profile.length > 1 && profile.endsWith("/")) profile = profile.slice(0, -1);
|
|
228
|
-
const profileExists = fs.existsSync(path.join(profile, "Local State"));
|
|
229
|
-
const result = spawnSync(process.execPath, [
|
|
230
|
-
resolver,
|
|
231
|
-
"resolve",
|
|
232
|
-
"--target",
|
|
233
|
-
path.resolve(target),
|
|
234
|
-
"--launch-method",
|
|
235
|
-
"launch-services",
|
|
236
|
-
"--display",
|
|
237
|
-
"headful",
|
|
238
|
-
...profileExists && fs.existsSync(record) ? ["--recorded", record] : [],
|
|
239
|
-
"--json"
|
|
240
|
-
], {
|
|
241
|
-
cwd: path.resolve(target),
|
|
242
|
-
encoding: "utf8",
|
|
243
|
-
timeout: 135e3
|
|
244
|
-
});
|
|
245
|
-
if (result.status === 0) {
|
|
246
|
-
const resolution = JSON.parse(result.stdout);
|
|
247
|
-
const managed = resolution.managedChrome;
|
|
248
|
-
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.` : "";
|
|
249
|
-
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}` : "";
|
|
250
|
-
return {
|
|
251
|
-
id: "extension-browser",
|
|
252
|
-
status: "pass",
|
|
253
|
-
required: false,
|
|
254
|
-
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}` : ""}`,
|
|
255
|
-
detail: resolution.bin
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
const message = (result.stderr || result.error?.message || "browser resolution failed").trim();
|
|
259
|
-
return {
|
|
260
|
-
id: "extension-browser",
|
|
261
|
-
status: "fail",
|
|
262
|
-
required: message.includes("BROWSER_UNLAUNCHABLE:"),
|
|
263
|
-
message,
|
|
264
|
-
userAction: "install Google Chrome, or set RECIPE_HARNESS_BROWSER / RECIPE_HARNESS_CHROME_BIN to a browser that starts"
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function mobileToolDoctorChecks(platform) {
|
|
268
|
-
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
269
|
-
{ tool: "adb", platform: "Android" },
|
|
270
|
-
{ tool: "idb", platform: "iOS" }
|
|
271
|
-
];
|
|
272
|
-
return specs.map(({ tool, platform: platformName }) => {
|
|
273
|
-
const resolved = resolveMobileToolPath(tool);
|
|
274
|
-
return {
|
|
275
|
-
id: `device-tool-${tool}`,
|
|
276
|
-
status: resolved ? "pass" : "fail",
|
|
277
|
-
required: platform !== void 0,
|
|
278
|
-
message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
|
|
279
|
-
...resolved ? { detail: resolved } : {},
|
|
280
|
-
...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
|
|
281
|
-
};
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
177
|
function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
|
|
285
178
|
if (isPin(runnerRoot)) return "pinned-install";
|
|
286
179
|
const normalizedRoot = path.normalize(runnerRoot);
|
|
@@ -344,11 +237,8 @@ function readJsonObject(file) {
|
|
|
344
237
|
export {
|
|
345
238
|
compatibilityMode,
|
|
346
239
|
createDoctorReport,
|
|
347
|
-
extensionBrowserDoctorCheck,
|
|
348
|
-
extensionBrowserResolutionDoctorCheck,
|
|
349
240
|
fixtureFileSummary,
|
|
350
241
|
fixtureSummary,
|
|
351
|
-
mobileToolDoctorChecks,
|
|
352
242
|
renderRuntimeContext,
|
|
353
243
|
repoShape,
|
|
354
244
|
requiredDoctorCheckSummary,
|
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/mm-harness-cli.js
CHANGED
|
@@ -1032,8 +1032,9 @@ function detectedSlotLine(out) {
|
|
|
1032
1032
|
if (ctx.slotId) parts.push(`slot ${out("ok", String(ctx.slotId))}`);
|
|
1033
1033
|
if (ctx.simulator) parts.push(`device ${out("ok", String(ctx.simulator))}`);
|
|
1034
1034
|
const devServerPort = ctx.watcherPort ?? ctx.devServerPort ?? ctx.metroPort;
|
|
1035
|
-
|
|
1036
|
-
|
|
1035
|
+
const surface = adapter ? getAdapterSurface(adapter) : void 0;
|
|
1036
|
+
if (surface && !surface.headless && devServerPort) {
|
|
1037
|
+
parts.push(`${surface.devServer.label} :${out("ok", String(devServerPort))}`);
|
|
1037
1038
|
}
|
|
1038
1039
|
if (ctx.gitBranch) parts.push(`branch ${out("info", String(ctx.gitBranch))}`);
|
|
1039
1040
|
if (parts.length === 0) return null;
|
package/dist/paths.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { adapterRegistry } from "./adapters/registry.js";
|
|
4
5
|
const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
5
6
|
const pathDefaults = readPathDefaults();
|
|
6
7
|
const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
|
|
@@ -88,8 +89,13 @@ function isProtocolRoot(candidate) {
|
|
|
88
89
|
return fs.existsSync(path.join(candidate, "packages/recipe-harness/package.json")) && fs.existsSync(path.join(candidate, "packages/protocol/package.json"));
|
|
89
90
|
}
|
|
90
91
|
function assertAdapter(adapter) {
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const ids = adapterRegistry.list();
|
|
93
|
+
if (ids.length === 0) {
|
|
94
|
+
throw new Error("internal: no adapters are registered yet; import src/adapters/surface.ts before validating an adapter");
|
|
95
|
+
}
|
|
96
|
+
if (typeof adapter !== "string" || !adapterRegistry.has(adapter)) {
|
|
97
|
+
const choices = ids.length > 1 ? `${ids.slice(0, -1).join(", ")}, or ${ids.at(-1)}` : ids.join("");
|
|
98
|
+
throw new Error(`Adapter must be ${choices}.`);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
function manifestPath(adapter) {
|
package/dist/runtime-context.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { getAdapterSurface } from "./adapters/surface.js";
|
|
6
7
|
import { recipeRuntimeDir } from "./paths.js";
|
|
7
8
|
function ensureRuntimeContext(target, adapter) {
|
|
8
9
|
const repoRoot = fs.realpathSync(target);
|
|
@@ -91,7 +92,7 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
|
|
|
91
92
|
if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
const forbiddenResources = adapter
|
|
95
|
+
const forbiddenResources = getAdapterSurface(adapter).runtimeContext.forbiddenFields;
|
|
95
96
|
if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
|
|
96
97
|
return true;
|
|
97
98
|
}
|
|
@@ -104,11 +105,12 @@ function localDefaults(repoRoot, adapter, existing) {
|
|
|
104
105
|
const digest = createHash("sha256").update(repoRoot).digest("hex");
|
|
105
106
|
const offset = Number.parseInt(digest.slice(0, 8), 16) % 1e4;
|
|
106
107
|
const defaults = { slotId: `local-${adapter}-${digest.slice(0, 8)}` };
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const localPorts = getAdapterSurface(adapter).runtimeContext.localPorts ?? {};
|
|
109
|
+
if (localPorts.cdp !== void 0 && !numericEnv("RECIPE_CDP_PORT", "CDP_PORT") && !existing.cdpPort) {
|
|
110
|
+
defaults.cdpPort = claimLocalPort(repoRoot, "cdp", localPorts.cdp + offset);
|
|
109
111
|
}
|
|
110
|
-
if (
|
|
111
|
-
defaults.watcherPort = claimLocalPort(repoRoot, "watcher",
|
|
112
|
+
if (localPorts.watcher !== void 0 && !numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") && !existing.watcherPort && !existing.metroPort && !existing.devServerPort) {
|
|
113
|
+
defaults.watcherPort = claimLocalPort(repoRoot, "watcher", localPorts.watcher + offset);
|
|
112
114
|
}
|
|
113
115
|
return defaults;
|
|
114
116
|
}
|
|
@@ -158,21 +160,14 @@ function portClaimIsLive(claim, port) {
|
|
|
158
160
|
}
|
|
159
161
|
}
|
|
160
162
|
function runtimeResources(adapter, existing, defaults) {
|
|
161
|
-
|
|
163
|
+
const { resources } = getAdapterSurface(adapter).runtimeContext;
|
|
164
|
+
if (!resources) return {};
|
|
162
165
|
const watcherPort = numericEnv("WATCHER_PORT", "METRO_PORT", "RECIPE_WATCHER_PORT") ?? existing.watcherPort ?? existing.metroPort ?? existing.devServerPort ?? defaults.watcherPort;
|
|
163
|
-
|
|
166
|
+
return {
|
|
164
167
|
watcherPort,
|
|
165
|
-
devServerPort: watcherPort
|
|
168
|
+
devServerPort: watcherPort,
|
|
169
|
+
...resources({ watcherPort, existing, defaults, envPort: numericEnv })
|
|
166
170
|
};
|
|
167
|
-
if (adapter === "extension" || adapter === "terminal") {
|
|
168
|
-
shared.cdpPort = numericEnv("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort;
|
|
169
|
-
} else {
|
|
170
|
-
shared.metroPort = watcherPort;
|
|
171
|
-
shared.simulator = process.env.IOS_SIMULATOR || existing.simulator;
|
|
172
|
-
shared.simulatorUdid = process.env.SIM_UDID || existing.simulatorUdid;
|
|
173
|
-
shared.adbSerial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial;
|
|
174
|
-
}
|
|
175
|
-
return shared;
|
|
176
171
|
}
|
|
177
172
|
function numericEnv(...names) {
|
|
178
173
|
for (const name of names) {
|