@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import { prepareMobile } from "../../adapters/mobile/prepare.js";
|
|
3
2
|
import {
|
|
4
3
|
mobileSourceCheck,
|
|
@@ -8,11 +7,7 @@ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
|
8
7
|
import { pinMobileFeatureFlags } from "../../adapters/mobile/feature-flags.js";
|
|
9
8
|
import { toRemoteFlagSpec } from "../../feature-flags.js";
|
|
10
9
|
import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
|
|
11
|
-
import { EXIT
|
|
12
|
-
import { applyDeviceTargeting } from "../device-target.js";
|
|
13
|
-
import { RECOVERY_CODE } from "../../heal-bounds.js";
|
|
14
|
-
import { recipeRuntimePath } from "../../paths.js";
|
|
15
|
-
import { shellQuote } from "./device.js";
|
|
10
|
+
import { EXIT } from "../shared.js";
|
|
16
11
|
async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false, remoteFlags) {
|
|
17
12
|
clearMobileReleaseArtifactState(target);
|
|
18
13
|
await ensureHarnessFresh(target, "mobile");
|
|
@@ -59,189 +54,6 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
|
|
|
59
54
|
}
|
|
60
55
|
return { ...result, remoteFeatureFlags: provenFlags };
|
|
61
56
|
}
|
|
62
|
-
function mobileProvisioningBlocked(output) {
|
|
63
|
-
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
64
|
-
}
|
|
65
|
-
function missingConfiguredIosSimulator(output) {
|
|
66
|
-
return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
|
|
67
|
-
}
|
|
68
|
-
function ambiguousConfiguredIosSimulator(output) {
|
|
69
|
-
return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
|
|
70
|
-
}
|
|
71
|
-
function mobileBridgeTargetMissing(output) {
|
|
72
|
-
return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
|
|
73
|
-
}
|
|
74
|
-
function mobileProvisionCommand(target, mobileTarget) {
|
|
75
|
-
const platform = mobileTarget === "android" ? "android" : "ios";
|
|
76
|
-
const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
|
|
77
|
-
const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
|
|
78
|
-
if (device) parts.push("--device", shellQuote(device));
|
|
79
|
-
return parts.join(" ");
|
|
80
|
-
}
|
|
81
|
-
function isIosSimulatorUdid(value) {
|
|
82
|
-
return /^[0-9A-Fa-f-]{36}$/u.test(value);
|
|
83
|
-
}
|
|
84
|
-
function launchDeviceLabel(mobileTarget) {
|
|
85
|
-
if (mobileTarget === "android") {
|
|
86
|
-
return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
|
|
87
|
-
}
|
|
88
|
-
if (mobileTarget === "ios") {
|
|
89
|
-
return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
|
|
90
|
-
}
|
|
91
|
-
return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
|
|
92
|
-
}
|
|
93
|
-
function metroLaunchEvidence(target) {
|
|
94
|
-
const evidencePath = recipeRuntimePath(target, "metro-generation.json");
|
|
95
|
-
let descriptor;
|
|
96
|
-
try {
|
|
97
|
-
descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
98
|
-
const stat = fs.fstatSync(descriptor);
|
|
99
|
-
if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
|
|
100
|
-
return JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
101
|
-
} catch {
|
|
102
|
-
return void 0;
|
|
103
|
-
} finally {
|
|
104
|
-
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
const mobileLaunchProfile = {
|
|
108
|
-
refuseFlags({ mobileTarget }) {
|
|
109
|
-
if (!mobileTarget) {
|
|
110
|
-
return { message: "target is required for mobile.", userAction: "mm-harness launch ios or mm-harness launch android" };
|
|
111
|
-
}
|
|
112
|
-
return void 0;
|
|
113
|
-
},
|
|
114
|
-
applyEnv({ adapter, options, mobileTarget, target }) {
|
|
115
|
-
const device = str(options, "device");
|
|
116
|
-
if (!device && mobileTarget !== "ios" && mobileTarget !== "android") return void 0;
|
|
117
|
-
const targetingOptions = {
|
|
118
|
-
...options,
|
|
119
|
-
...mobileTarget ? { platform: mobileTarget } : {}
|
|
120
|
-
};
|
|
121
|
-
const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
|
|
122
|
-
if ("code" in result) {
|
|
123
|
-
if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
|
|
124
|
-
process.env.IOS_SIMULATOR = device;
|
|
125
|
-
process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
|
|
126
|
-
process.env.ADB_SERIAL = "";
|
|
127
|
-
process.env.ANDROID_SERIAL = "";
|
|
128
|
-
process.env.ANDROID_DEVICE = "";
|
|
129
|
-
process.env.ANDROID_TARGET_DEVICE_NAME = "";
|
|
130
|
-
} else {
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
|
|
135
|
-
return {
|
|
136
|
-
ok: false,
|
|
137
|
-
code: "DEVICE_WRONG_PLATFORM",
|
|
138
|
-
message: `--device ${device} did not resolve to an Android device for launch android.`,
|
|
139
|
-
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
|
|
143
|
-
return {
|
|
144
|
-
ok: false,
|
|
145
|
-
code: "DEVICE_WRONG_PLATFORM",
|
|
146
|
-
message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
|
|
147
|
-
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return void 0;
|
|
151
|
-
},
|
|
152
|
-
start({ options, mobileTarget, machine, tier, wantClearMetro }) {
|
|
153
|
-
const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
|
|
154
|
-
const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
|
|
155
|
-
writeInteractiveProgress(
|
|
156
|
-
machine,
|
|
157
|
-
`\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
|
|
158
|
-
);
|
|
159
|
-
},
|
|
160
|
-
nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
|
|
161
|
-
rebuildCommand: ({ mobileTarget }) => `mm-harness launch ${mobileTarget} --build`,
|
|
162
|
-
compose: ({ target, mobileTarget, machine, remoteFlagRequest }, tier, restartApp, clearMetro) => launchMobile(target, mobileTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags),
|
|
163
|
-
classify(_launch, output) {
|
|
164
|
-
if (output.includes("open-device: Android device is locked.")) {
|
|
165
|
-
return {
|
|
166
|
-
code: "MOBILE_DEVICE_LOCKED",
|
|
167
|
-
message: "the selected Android device is locked.",
|
|
168
|
-
recoverable: false,
|
|
169
|
-
userAction: "Unlock the selected Android device, then retry the launch.",
|
|
170
|
-
exitCode: EXIT.runtime,
|
|
171
|
-
originalError: output.trim()
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
if (/open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(output)) {
|
|
175
|
-
return {
|
|
176
|
-
code: "MOBILE_DEVICE_UI_UNAVAILABLE",
|
|
177
|
-
message: "the selected Android display could not be prepared.",
|
|
178
|
-
recoverable: false,
|
|
179
|
-
userAction: "Check adb access and the selected device screen, then retry the launch.",
|
|
180
|
-
exitCode: EXIT.runtime,
|
|
181
|
-
originalError: output.trim()
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
return void 0;
|
|
185
|
-
},
|
|
186
|
-
async beforeHeal(launch, attempt, state) {
|
|
187
|
-
const { target, mobileTarget, tier } = launch;
|
|
188
|
-
const output = attempt.output;
|
|
189
|
-
const missingIosSimulator = missingConfiguredIosSimulator(output);
|
|
190
|
-
const ambiguousIosSimulator = ambiguousConfiguredIosSimulator(output);
|
|
191
|
-
if (ambiguousIosSimulator) {
|
|
192
|
-
return launch.fail(state, {
|
|
193
|
-
code: "MOBILE_DEVICE_AMBIGUOUS",
|
|
194
|
-
message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
|
|
195
|
-
recoverable: false,
|
|
196
|
-
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
197
|
-
exitCode: EXIT.runtime,
|
|
198
|
-
originalError: output.trim() || void 0
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
|
|
202
|
-
return launch.fail(state, {
|
|
203
|
-
code: "MOBILE_DEVICE_NOT_FOUND",
|
|
204
|
-
message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
|
|
205
|
-
recoverable: false,
|
|
206
|
-
userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
|
|
207
|
-
exitCode: EXIT.runtime,
|
|
208
|
-
originalError: output.trim() || void 0
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
if (mobileProvisioningBlocked(output)) {
|
|
212
|
-
return launch.fail(state, {
|
|
213
|
-
code: "MOBILE_PROVISION_REQUIRED",
|
|
214
|
-
message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
|
|
215
|
-
recoverable: false,
|
|
216
|
-
userAction: mobileProvisionCommand(target, mobileTarget),
|
|
217
|
-
exitCode: EXIT.runtime,
|
|
218
|
-
originalError: output.trim() || void 0
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
if (tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(output)) {
|
|
222
|
-
return launch.fail(state, {
|
|
223
|
-
code: "MOBILE_NATIVE_BUILD_FAILED",
|
|
224
|
-
message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
|
|
225
|
-
recoverable: false,
|
|
226
|
-
exitCode: EXIT.runtime,
|
|
227
|
-
originalError: output.trim() || void 0
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
return void 0;
|
|
231
|
-
},
|
|
232
|
-
recovery(_launch, output) {
|
|
233
|
-
const restartApp = mobileBridgeTargetMissing(output);
|
|
234
|
-
return { code: restartApp ? "mobile.app-restarted" : RECOVERY_CODE.mobile, restartApp };
|
|
235
|
-
},
|
|
236
|
-
// Mobile proved its set inside launchMobile (null: nothing requested and no
|
|
237
|
-
// runtime answered).
|
|
238
|
-
provenFlags: (_launch, requested, proven) => ({ flags: proven === void 0 ? requested : proven }),
|
|
239
|
-
verifyArgs: ({ mobileTarget }) => mobileTarget ? ["--platform", mobileTarget] : [],
|
|
240
|
-
platformLabel: ({ mobileTarget }) => mobileTarget ?? null,
|
|
241
|
-
deviceLabel: ({ mobileTarget }) => launchDeviceLabel(mobileTarget),
|
|
242
|
-
evidence: metroLaunchEvidence
|
|
243
|
-
};
|
|
244
57
|
export {
|
|
245
|
-
launchMobile
|
|
246
|
-
mobileLaunchProfile
|
|
58
|
+
launchMobile
|
|
247
59
|
};
|
package/dist/commands/logs.js
CHANGED
|
@@ -68,7 +68,7 @@ async function handleLogs(argv) {
|
|
|
68
68
|
if (flag(options, "window")) {
|
|
69
69
|
surface.resolveSlotPorts(target);
|
|
70
70
|
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
|
|
71
|
-
const windowName =
|
|
71
|
+
const windowName = `${surface.devServer.label}-${port}`;
|
|
72
72
|
const devLog = logSources[0].path;
|
|
73
73
|
const leaf = path.join(runnerDir, "adapters/shared/open-log-window.sh");
|
|
74
74
|
const result = spawnScript(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { detectAdapter } from "../harness.js";
|
|
3
|
-
import {
|
|
3
|
+
import { adapterRegistry } from "../adapters/registry.js";
|
|
4
|
+
import { assertAdapter } from "../paths.js";
|
|
4
5
|
import { explicitRecipeTrustOptions } from "../recipe-security.js";
|
|
5
6
|
import { ADAPTER_DETECT_NEXT } from "./shared.js";
|
|
6
7
|
import { EXIT } from "./shared.js";
|
|
@@ -179,12 +180,16 @@ function requiredOption(options, key, message) {
|
|
|
179
180
|
if (!value) throw usageError(message);
|
|
180
181
|
return value;
|
|
181
182
|
}
|
|
183
|
+
function adapterUsageError(error) {
|
|
184
|
+
if (error instanceof Error && error.message.startsWith("internal:")) return error;
|
|
185
|
+
return usageError(error instanceof Error ? error.message : String(error));
|
|
186
|
+
}
|
|
182
187
|
function adapterOption(options) {
|
|
183
188
|
const adapter = optionString(options, "adapter");
|
|
184
189
|
try {
|
|
185
190
|
assertAdapter(adapter);
|
|
186
191
|
} catch (error) {
|
|
187
|
-
throw
|
|
192
|
+
throw adapterUsageError(error);
|
|
188
193
|
}
|
|
189
194
|
return adapter;
|
|
190
195
|
}
|
|
@@ -193,7 +198,7 @@ function targetPath(options) {
|
|
|
193
198
|
}
|
|
194
199
|
function actionManifestPathOption(options, adapter) {
|
|
195
200
|
const configured = optionString(options, "actionManifest");
|
|
196
|
-
return configured ? path.resolve(configured) :
|
|
201
|
+
return configured ? path.resolve(configured) : adapterRegistry.get(adapter).actions.manifestPath();
|
|
197
202
|
}
|
|
198
203
|
function parsePort(value, errorMessage) {
|
|
199
204
|
const port = Number(value);
|
|
@@ -238,7 +243,7 @@ function resolveAdapter(options) {
|
|
|
238
243
|
try {
|
|
239
244
|
assertAdapter(adapter);
|
|
240
245
|
} catch (error) {
|
|
241
|
-
throw
|
|
246
|
+
throw adapterUsageError(error);
|
|
242
247
|
}
|
|
243
248
|
return { adapter, target };
|
|
244
249
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { isPin, PIN_MARKER } from "../harness-pin.js";
|
|
8
|
+
import { runnerDir } from "../paths.js";
|
|
9
|
+
import { EXIT } from "./shared.js";
|
|
10
|
+
const PACKAGE_NAME = "@deeeed/metamask-harness";
|
|
11
|
+
const STALE_STAGING_MS = 30 * 60 * 1e3;
|
|
12
|
+
const RUNNER_STATE = /* @__PURE__ */ new Set([".omc", ".omx"]);
|
|
13
|
+
function pinRoot(env = process.env, home = os.homedir()) {
|
|
14
|
+
return path.resolve(env.MM_HARNESS_PIN_ROOT || path.join(home, ".local/share/mm-harness/pins"));
|
|
15
|
+
}
|
|
16
|
+
function readManifest(root) {
|
|
17
|
+
return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
18
|
+
}
|
|
19
|
+
function contentDigest(root) {
|
|
20
|
+
const hash = crypto.createHash("sha256");
|
|
21
|
+
const walk = (directory, relative) => {
|
|
22
|
+
hash.update(`d\0${relative}\0`);
|
|
23
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (RUNNER_STATE.has(entry.name) || !relative && entry.name === PIN_MARKER) continue;
|
|
26
|
+
const absolute = path.join(directory, entry.name);
|
|
27
|
+
const child = relative ? `${relative}/${entry.name}` : entry.name;
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
walk(absolute, child);
|
|
30
|
+
} else if (entry.isSymbolicLink()) {
|
|
31
|
+
hash.update(`l\0${child}\0${fs.readlinkSync(absolute)}\0`);
|
|
32
|
+
} else if (entry.isFile()) {
|
|
33
|
+
const fd = fs.openSync(absolute, "r");
|
|
34
|
+
try {
|
|
35
|
+
const opened = fs.fstatSync(fd);
|
|
36
|
+
hash.update(`f\0${child}\0${opened.mode & 73 ? "x" : "-"}\0${opened.size}\0`);
|
|
37
|
+
hash.update(fs.readFileSync(fd));
|
|
38
|
+
hash.update("\0");
|
|
39
|
+
} finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(root, "");
|
|
46
|
+
return hash.digest("hex");
|
|
47
|
+
}
|
|
48
|
+
function relinkIntoCopy(copy, sourceAliases) {
|
|
49
|
+
const walk = (directory) => {
|
|
50
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
51
|
+
const absolute = path.join(directory, entry.name);
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
walk(absolute);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!entry.isSymbolicLink()) continue;
|
|
57
|
+
const raw = fs.readlinkSync(absolute);
|
|
58
|
+
if (!path.isAbsolute(raw)) continue;
|
|
59
|
+
const targets = [path.resolve(raw)];
|
|
60
|
+
try {
|
|
61
|
+
targets.push(fs.realpathSync(raw));
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
let match;
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
const alias = sourceAliases.find((prefix) => target === prefix || target.startsWith(`${prefix}${path.sep}`));
|
|
67
|
+
if (alias) {
|
|
68
|
+
match = { alias, target };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
fs.unlinkSync(absolute);
|
|
74
|
+
fs.symlinkSync(path.relative(directory, path.join(copy, path.relative(match.alias, match.target))) || ".", absolute);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(copy);
|
|
78
|
+
}
|
|
79
|
+
function assertSelfContained(root) {
|
|
80
|
+
const manifest = readManifest(root);
|
|
81
|
+
const lookup = createRequire(path.join(root, "package.json")).resolve.paths("") ?? [];
|
|
82
|
+
const realRoot = fs.realpathSync(root);
|
|
83
|
+
const inside = (candidate) => candidate === realRoot || candidate.startsWith(`${realRoot}${path.sep}`);
|
|
84
|
+
for (const dependency of Object.keys(manifest.dependencies ?? {})) {
|
|
85
|
+
const found = lookup.map((directory) => path.join(directory, dependency)).find((candidate) => fs.existsSync(path.join(candidate, "package.json")));
|
|
86
|
+
if (!found) throw new Error(`dependency ${dependency} is not installed for ${root}`);
|
|
87
|
+
const resolved = fs.realpathSync(found);
|
|
88
|
+
if (!inside(resolved)) {
|
|
89
|
+
throw new Error(`dependency ${dependency} resolves outside the install (${resolved}); only a self-contained install (npm i -g) can be pinned`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const walk = (directory) => {
|
|
93
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
94
|
+
const absolute = path.join(directory, entry.name);
|
|
95
|
+
if (entry.isDirectory()) walk(absolute);
|
|
96
|
+
else if (entry.isSymbolicLink() && path.isAbsolute(fs.readlinkSync(absolute))) {
|
|
97
|
+
let resolved;
|
|
98
|
+
try {
|
|
99
|
+
resolved = fs.realpathSync(absolute);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!inside(resolved)) throw new Error(`link ${path.relative(root, absolute)} leads outside the install (${resolved})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(root);
|
|
108
|
+
}
|
|
109
|
+
function readPinMarker(pin) {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(fs.readFileSync(path.join(pin, PIN_MARKER), "utf8"));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === "ENOENT") return void 0;
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function assertPinUnchanged(pin, marker) {
|
|
118
|
+
if (typeof marker.contentSha256 !== "string" || marker.contentSha256 !== contentDigest(pin)) {
|
|
119
|
+
throw new Error(`pin ${pin} changed after it was created; remove it, then re-run: mm-harness pin`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function sweepStaleStaging(store, now = Date.now()) {
|
|
123
|
+
let names;
|
|
124
|
+
try {
|
|
125
|
+
names = fs.readdirSync(store);
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
const created = /^\.staging-.+-(\d{13})-[^-]+$/u.exec(name)?.[1];
|
|
131
|
+
if (created && now - Number(created) > STALE_STAGING_MS) {
|
|
132
|
+
fs.rmSync(path.join(store, name), { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function pinHarness(options = {}) {
|
|
137
|
+
const sourceAlias = path.resolve(options.sourceRoot ?? runnerDir);
|
|
138
|
+
const source = fs.realpathSync(sourceAlias);
|
|
139
|
+
const store = options.root ?? pinRoot();
|
|
140
|
+
const manifest = readManifest(source);
|
|
141
|
+
if (manifest.name !== PACKAGE_NAME || typeof manifest.version !== "string" || !manifest.version) {
|
|
142
|
+
throw new Error(`not an ${PACKAGE_NAME} package: ${source}`);
|
|
143
|
+
}
|
|
144
|
+
const version = manifest.version;
|
|
145
|
+
const ownExecutable = path.join(source, "bin/mm-harness");
|
|
146
|
+
if (isPin(source)) {
|
|
147
|
+
return { version, source, executable: ownExecutable, pinned: true, created: false, reason: "already-pinned" };
|
|
148
|
+
}
|
|
149
|
+
if (!source.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
150
|
+
return { version, source, executable: ownExecutable, pinned: false, created: false, reason: "source-checkout" };
|
|
151
|
+
}
|
|
152
|
+
const key = `${version}-${contentDigest(source).slice(0, 12)}`;
|
|
153
|
+
const target = path.join(store, key);
|
|
154
|
+
const executable = path.join(target, "bin/mm-harness");
|
|
155
|
+
const existing = readPinMarker(target);
|
|
156
|
+
if (existing) {
|
|
157
|
+
assertPinUnchanged(target, existing);
|
|
158
|
+
return { version, source, executable, pinned: true, created: false };
|
|
159
|
+
}
|
|
160
|
+
assertSelfContained(source);
|
|
161
|
+
fs.mkdirSync(store, { recursive: true });
|
|
162
|
+
sweepStaleStaging(store);
|
|
163
|
+
const staging = fs.mkdtempSync(path.join(store, `.staging-${key}-${Date.now()}-`));
|
|
164
|
+
const copy = path.join(staging, "package");
|
|
165
|
+
try {
|
|
166
|
+
fs.cpSync(source, copy, {
|
|
167
|
+
recursive: true,
|
|
168
|
+
preserveTimestamps: true,
|
|
169
|
+
verbatimSymlinks: true,
|
|
170
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
171
|
+
filter: (from) => !RUNNER_STATE.has(path.basename(from))
|
|
172
|
+
});
|
|
173
|
+
relinkIntoCopy(copy, [.../* @__PURE__ */ new Set([source, sourceAlias])]);
|
|
174
|
+
assertSelfContained(copy);
|
|
175
|
+
const check = spawnSync(path.join(copy, "bin/mm-harness"), ["--version"], {
|
|
176
|
+
encoding: "utf8",
|
|
177
|
+
env: { ...process.env, MM_HARNESS_BIN: "", MM_HARNESS_NO_UPDATE_CHECK: "1" },
|
|
178
|
+
timeout: 6e4
|
|
179
|
+
});
|
|
180
|
+
if (check.status !== 0 || check.stdout.trim() !== version) {
|
|
181
|
+
throw new Error(`the copied harness did not run (exit ${check.status}): ${(check.stderr || check.stdout || "").trim()}`);
|
|
182
|
+
}
|
|
183
|
+
const marker = { schemaVersion: 1, key, version, source, contentSha256: contentDigest(copy), createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
184
|
+
fs.writeFileSync(path.join(copy, PIN_MARKER), `${JSON.stringify(marker, null, 2)}
|
|
185
|
+
`);
|
|
186
|
+
try {
|
|
187
|
+
fs.renameSync(copy, target);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (!isPin(target)) throw error;
|
|
190
|
+
return { version, source, executable, pinned: true, created: false };
|
|
191
|
+
}
|
|
192
|
+
return { version, source, executable, pinned: true, created: true };
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function handlePin(argv) {
|
|
198
|
+
const json = argv.includes("--json");
|
|
199
|
+
try {
|
|
200
|
+
const result = pinHarness();
|
|
201
|
+
if (json) {
|
|
202
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "pin", status: "pass", ...result }, null, 2));
|
|
203
|
+
} else {
|
|
204
|
+
console.log(result.executable);
|
|
205
|
+
if (result.reason === "source-checkout") {
|
|
206
|
+
console.error("mm-harness pin: a source checkout is not copied; record it by its working-tree hash.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return EXIT.ok;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
const userAction = "install the harness globally (npm i -g @deeeed/metamask-harness), then re-run: mm-harness pin";
|
|
213
|
+
if (json) {
|
|
214
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "pin", status: "fail", error: { code: "PIN_FAILED", message, userAction } }, null, 2));
|
|
215
|
+
} else {
|
|
216
|
+
console.error(`\u2717 mm-harness pin: ${message}
|
|
217
|
+
Next: ${userAction}`);
|
|
218
|
+
}
|
|
219
|
+
return EXIT.runtime;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
handlePin,
|
|
224
|
+
pinHarness,
|
|
225
|
+
pinRoot
|
|
226
|
+
};
|
package/dist/commands/prepare.js
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
import { color, stripAnsi } from "../cli-color.js";
|
|
11
11
|
import { runnerProvenance } from "../doctor.js";
|
|
12
12
|
import { detectAdapter } from "../harness.js";
|
|
13
|
-
import {
|
|
13
|
+
import { adapterRegistry } from "../adapters/registry.js";
|
|
14
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
15
|
+
import { harnessExecutable, PREPARE_PROGRESS_ARTIFACT, recipeRuntimeDir } from "../paths.js";
|
|
14
16
|
import { optionFlag, optionString, parseArgs, targetPath, usageError } from "./parse-args.js";
|
|
15
17
|
import { EXIT } from "./shared.js";
|
|
16
18
|
import { formatDuration } from "./status-watch.js";
|
|
@@ -52,8 +54,8 @@ const STEP_HINT = {
|
|
|
52
54
|
launch: "can take minutes on Mobile: Metro bundle and app start",
|
|
53
55
|
fixtures: "under a minute"
|
|
54
56
|
};
|
|
55
|
-
function stepChain(
|
|
56
|
-
return
|
|
57
|
+
function stepChain(surface) {
|
|
58
|
+
return surface.headless ? ["doctor", "status", "verify"] : ["doctor", "status", "launch", "fixtures", "verify"];
|
|
57
59
|
}
|
|
58
60
|
function lastRunDurations(recordPath) {
|
|
59
61
|
let parsed;
|
|
@@ -225,36 +227,6 @@ function readDevices(artifactsDir) {
|
|
|
225
227
|
const devices = parsed?.devices;
|
|
226
228
|
return Array.isArray(devices) ? devices : [];
|
|
227
229
|
}
|
|
228
|
-
function readRuntimeContextTarget(target) {
|
|
229
|
-
const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
|
|
230
|
-
let parsed;
|
|
231
|
-
try {
|
|
232
|
-
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
233
|
-
} catch {
|
|
234
|
-
return {};
|
|
235
|
-
}
|
|
236
|
-
const record = parsed ?? {};
|
|
237
|
-
const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
|
|
238
|
-
return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
|
|
239
|
-
}
|
|
240
|
-
function mobilePlatformFromConfiguredTarget(env, context) {
|
|
241
|
-
const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
|
|
242
|
-
const android = Boolean(
|
|
243
|
-
env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
|
|
244
|
-
);
|
|
245
|
-
if (ios === android) return void 0;
|
|
246
|
-
return ios ? "ios" : "android";
|
|
247
|
-
}
|
|
248
|
-
function mobilePlatformFromDevices(devices) {
|
|
249
|
-
const selected = devices.filter((device) => device?.selected === true);
|
|
250
|
-
const candidates = selected.length > 0 ? selected : devices;
|
|
251
|
-
const platforms = [
|
|
252
|
-
...new Set(
|
|
253
|
-
candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
|
|
254
|
-
)
|
|
255
|
-
];
|
|
256
|
-
return platforms.length === 1 ? platforms[0] : void 0;
|
|
257
|
-
}
|
|
258
230
|
function mobileTargetAmbiguityError() {
|
|
259
231
|
const choices = [
|
|
260
232
|
"1. iOS mm-harness prepare --platform mobile --mobile-platform ios",
|
|
@@ -300,8 +272,9 @@ function mobilePlatformOption(value) {
|
|
|
300
272
|
}
|
|
301
273
|
function resolvePlatform(requested, target) {
|
|
302
274
|
const platform = requested ?? detectAdapter(target);
|
|
303
|
-
|
|
304
|
-
|
|
275
|
+
const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
|
|
276
|
+
if (surface?.prepare) {
|
|
277
|
+
return surface;
|
|
305
278
|
}
|
|
306
279
|
throw usageError(
|
|
307
280
|
`could not detect the platform of ${target}
|
|
@@ -328,21 +301,22 @@ async function handlePrepareLocked(argv) {
|
|
|
328
301
|
const { options } = parseArgs(argv, "prepare");
|
|
329
302
|
const json = optionFlag(options, "json");
|
|
330
303
|
const target = targetPath(options);
|
|
331
|
-
const
|
|
304
|
+
const surface = resolvePlatform(optionString(options, "platform"), target);
|
|
305
|
+
const platform = surface.adapter;
|
|
332
306
|
const artifactsDir = path.resolve(
|
|
333
307
|
optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
|
|
334
308
|
);
|
|
335
309
|
const executable = harnessExecutable();
|
|
336
310
|
const targetArgs = ["--target", target];
|
|
337
311
|
const clearMetro = optionFlag(options, "clearMetro");
|
|
338
|
-
if (clearMetro &&
|
|
312
|
+
if (clearMetro && !surface.prepare.clearMetro) {
|
|
339
313
|
throw usageError(`--clear-metro is mobile only; this checkout is ${platform}
|
|
340
314
|
Next: ${USAGE}`);
|
|
341
315
|
}
|
|
342
316
|
const remoteFlag = optionString(options, "remoteFlag");
|
|
343
317
|
if (remoteFlag !== void 0) {
|
|
344
|
-
if (
|
|
345
|
-
throw usageError(`--remote-flag needs an app runtime; this checkout is
|
|
318
|
+
if (surface.headless) {
|
|
319
|
+
throw usageError(`--remote-flag needs an app runtime; this checkout is ${platform}
|
|
346
320
|
Next: ${USAGE}`);
|
|
347
321
|
}
|
|
348
322
|
try {
|
|
@@ -357,7 +331,7 @@ async function handlePrepareLocked(argv) {
|
|
|
357
331
|
let mobilePlatform = mobilePlatformOption(optionString(options, "mobilePlatform"));
|
|
358
332
|
const recordPath = path.join(artifactsDir, READINESS_RECORD);
|
|
359
333
|
const progressPath = path.join(artifactsDir, PREPARE_PROGRESS_ARTIFACT);
|
|
360
|
-
const chain = stepChain(
|
|
334
|
+
const chain = stepChain(surface);
|
|
361
335
|
const view = json ? void 0 : createStepView(chain, stepHints(chain, lastRunDurations(recordPath)));
|
|
362
336
|
view?.begin();
|
|
363
337
|
const steps = [];
|
|
@@ -406,35 +380,33 @@ async function handlePrepareLocked(argv) {
|
|
|
406
380
|
try {
|
|
407
381
|
await run("doctor", ["doctor", "--fix", "--json", "--adapter", platform, ...targetArgs]);
|
|
408
382
|
await run("status", ["status", "--json", ...targetArgs]);
|
|
409
|
-
|
|
410
|
-
|
|
383
|
+
const { devicePlatform } = surface.prepare;
|
|
384
|
+
if (devicePlatform && !mobilePlatform && !failed()) {
|
|
385
|
+
mobilePlatform = devicePlatform(target, readDevices(artifactsDir));
|
|
411
386
|
if (!mobilePlatform) return ambiguousMobileTarget(json);
|
|
412
387
|
}
|
|
413
|
-
if (
|
|
414
|
-
const reason =
|
|
388
|
+
if (surface.headless) {
|
|
389
|
+
const reason = `${platform} is headless: no app surface to launch or seed`;
|
|
415
390
|
addStep(skippedStep("launch", `${executable} launch`, reason));
|
|
416
391
|
addStep(skippedStep("fixtures", `${executable} fixtures set`, reason));
|
|
417
|
-
} else
|
|
418
|
-
const device = mobilePlatform ? [mobilePlatform] : [];
|
|
392
|
+
} else {
|
|
393
|
+
const device = devicePlatform && mobilePlatform ? [mobilePlatform] : [];
|
|
419
394
|
await run("launch", ["launch", ...device, ...clearMetro ? ["--clear-metro"] : [], ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
420
395
|
await run("fixtures", [
|
|
421
396
|
"fixtures",
|
|
422
397
|
"set",
|
|
423
|
-
...
|
|
398
|
+
...device.flatMap((platformName) => ["--platform", platformName]),
|
|
424
399
|
...remoteFlagArgs,
|
|
425
400
|
"--json",
|
|
426
401
|
...targetArgs
|
|
427
402
|
]);
|
|
428
|
-
} else {
|
|
429
|
-
await run("launch", ["launch", ...remoteFlagArgs, "--verify", "--json", ...targetArgs]);
|
|
430
|
-
await run("fixtures", ["fixtures", "set", ...remoteFlagArgs, "--json", ...targetArgs]);
|
|
431
403
|
}
|
|
432
404
|
await run("verify", ["verify", "--json", "--adapter", platform, ...targetArgs]);
|
|
433
405
|
} finally {
|
|
434
406
|
clearInterval(heartbeat);
|
|
435
407
|
view?.end();
|
|
436
408
|
}
|
|
437
|
-
const provenance = runnerProvenance(manifestPath(
|
|
409
|
+
const provenance = runnerProvenance(surface.actions.manifestPath());
|
|
438
410
|
const record = {
|
|
439
411
|
schemaVersion: 1,
|
|
440
412
|
harness: {
|
|
@@ -462,7 +434,5 @@ async function handlePrepareLocked(argv) {
|
|
|
462
434
|
}
|
|
463
435
|
export {
|
|
464
436
|
handlePrepare,
|
|
465
|
-
mobilePlatformFromConfiguredTarget,
|
|
466
|
-
mobilePlatformFromDevices,
|
|
467
437
|
mobileTargetAmbiguityError
|
|
468
438
|
};
|