@deeeed/metamask-harness 0.14.5 → 0.15.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 +96 -6
- package/adapters/extension/ensure-browser.sh +4 -2
- package/adapters/extension/launch-browser.cjs +73 -34
- package/adapters/extension/live.sh +21 -10
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/sidepanel-toggle.sh +10 -4
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/verify.sh +19 -2
- package/adapters/extension/wallet-fixture-state.cjs +14 -1
- package/adapters/manifest.json +19 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
- package/adapters/mobile/launch-metro.cjs +74 -0
- package/adapters/mobile/lib/tmux-viewer.sh +4 -4
- package/adapters/mobile/open-device.sh +3 -1
- package/adapters/mobile/start-metro.sh +12 -18
- package/adapters/mobile/stop-metro.sh +1 -0
- package/adapters/mobile/verify.sh +5 -5
- package/adapters/shared/install-repo-deps.sh +29 -0
- package/adapters/shared/open-debug.mjs +35 -4
- package/adapters/shared/open-log-window.sh +1 -1
- package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
- package/adapters/shared/resolve-slot-ports.sh +21 -17
- package/adapters/shared/tmux-session.sh +0 -5
- package/adapters/shared/tmux-viewer.sh +7 -2
- package/dist/adapters/core/surface.js +3 -0
- package/dist/adapters/extension/ensure-ready.js +0 -7
- package/dist/adapters/extension/runtime-decision.js +93 -3
- package/dist/adapters/extension/surface.js +17 -6
- package/dist/adapters/mobile/metro-env.js +67 -0
- package/dist/adapters/mobile/perps-env.js +101 -0
- package/dist/adapters/mobile/prepare.js +32 -14
- package/dist/adapters/mobile/runtime-decision.js +21 -2
- package/dist/adapters/mobile/source-freshness.js +116 -0
- package/dist/adapters/mobile/surface.js +5 -1
- package/dist/adapters/resolve-slot-ports.js +4 -0
- package/dist/adapters/slot-ports.js +131 -49
- package/dist/adapters.js +7 -2
- package/dist/checkout-lock.js +72 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -0
- package/dist/commands/call.js +91 -54
- package/dist/commands/check.js +266 -46
- package/dist/commands/checklist.js +136 -0
- package/dist/commands/debug.js +21 -2
- package/dist/commands/doctor.js +233 -18
- package/dist/commands/farmslot-ready.js +25 -0
- package/dist/commands/fixtures.js +177 -42
- package/dist/commands/launch/extension.js +6 -1
- package/dist/commands/launch/index.js +54 -22
- package/dist/commands/logs.js +24 -4
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run-engine.js +223 -7
- package/dist/commands/run.js +67 -50
- package/dist/commands/shared.js +86 -5
- package/dist/commands/stop.js +1 -2
- package/dist/doctor.js +39 -17
- package/dist/harness.js +5 -2
- package/dist/heal-bounds.js +1 -1
- package/dist/mm-harness-cli.js +41 -12
- package/dist/runner.js +35 -5
- package/dist/runtime-context.js +228 -0
- package/docs/CLI-SPEC.md +15 -11
- package/docs/MENTAL-MODEL.md +6 -6
- package/library/actions/extension/perps/perps.mjs +29 -8
- package/library/actions/extension/platform/cdp.mjs +128 -28
- package/library/actions/extension/ui/navigate.mjs +1 -1
- package/library/actions/mobile/perps/perps.mjs +13 -1
- package/library/actions/mobile/platform/bridge.mjs +302 -29
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +7 -2
- package/library/manifests/extension.action-manifest.json +32 -12
- package/library/manifests/mobile.action-manifest.json +25 -3
- package/package.json +4 -4
- package/scripts/completions.sh +7 -4
package/dist/commands/doctor.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { color } from "../cli-color.js";
|
|
4
|
-
import { createDoctorReport, renderRuntimeContext } from "../doctor.js";
|
|
6
|
+
import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDoctorCheckSummary } from "../doctor.js";
|
|
5
7
|
import { detectAdapter } from "../harness.js";
|
|
6
8
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
7
9
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
10
|
+
import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
|
|
8
11
|
import { loadActionManifest, validateManifest } from "../manifest.js";
|
|
9
12
|
import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
|
|
10
13
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
@@ -15,7 +18,10 @@ import {
|
|
|
15
18
|
renderMobileDeviceList,
|
|
16
19
|
renderMobileLiveBlock
|
|
17
20
|
} from "./mobile-device-view.js";
|
|
18
|
-
import {
|
|
21
|
+
import { farmslotReadyIndicator } from "./farmslot-ready.js";
|
|
22
|
+
import { ensureRuntimeContext } from "../runtime-context.js";
|
|
23
|
+
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
24
|
+
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
|
|
19
25
|
import {
|
|
20
26
|
actionManifestPathOption,
|
|
21
27
|
applyRuntimeDirOption,
|
|
@@ -40,7 +46,8 @@ async function handleDoctor({ options }) {
|
|
|
40
46
|
applyRuntimeDirOption(options);
|
|
41
47
|
const target = targetPath(options);
|
|
42
48
|
const json = optionFlag(options, "json");
|
|
43
|
-
const
|
|
49
|
+
const printReady = optionFlag(options, "printReady");
|
|
50
|
+
const expectLive = optionFlag(options, "expectLive") || printReady;
|
|
44
51
|
const allDevices = optionFlag(options, "allDevices");
|
|
45
52
|
const platformOption = optionString(options, "platform");
|
|
46
53
|
const explicitAdapter = optionString(options, "adapter") ?? (platformOption === "ios" || platformOption === "android" ? "mobile" : platformOption);
|
|
@@ -49,18 +56,57 @@ async function handleDoctor({ options }) {
|
|
|
49
56
|
return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
50
57
|
}
|
|
51
58
|
assertAdapter(adapter);
|
|
59
|
+
if (!fs.existsSync(target)) {
|
|
60
|
+
return usageOut(
|
|
61
|
+
json,
|
|
62
|
+
"doctor",
|
|
63
|
+
`target does not exist: ${target}`,
|
|
64
|
+
"pass --target <metamask-checkout> pointing to an existing checkout"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (printReady && json) {
|
|
68
|
+
return usageOut(
|
|
69
|
+
json,
|
|
70
|
+
"doctor",
|
|
71
|
+
"--print-ready owns stdout for Farmslot health_check; drop --json (use --expect-live --json for the doctor envelope)",
|
|
72
|
+
"mm-harness doctor --print-ready --adapter <adapter> --target <path>"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
52
75
|
const actionManifestPath = actionManifestPathOption(options, adapter);
|
|
53
76
|
const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
|
|
54
77
|
const manifestValidation = await validateManifest(manifest);
|
|
55
78
|
if (optionFlag(options, "fix")) {
|
|
56
|
-
const
|
|
79
|
+
const surface = getAdapterSurface(adapter);
|
|
80
|
+
surface.resolveSlotPorts(target);
|
|
81
|
+
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
82
|
+
if ("code" in dtResult) {
|
|
83
|
+
return usageOut(json, "doctor", dtResult.message, "mm-harness doctor --fix --adapter mobile --device <adb-serial|simulator-udid>");
|
|
84
|
+
}
|
|
85
|
+
applyDoctorRuntimePorts(options);
|
|
86
|
+
const lock = acquireCheckoutLock(target, "doctor-fix");
|
|
87
|
+
if ("message" in lock) {
|
|
88
|
+
return checkoutBusyOut(json, "doctor", lock.message, lock.path);
|
|
89
|
+
}
|
|
90
|
+
let fixed;
|
|
91
|
+
let failed;
|
|
92
|
+
try {
|
|
93
|
+
({ fixed, failed } = await runDoctorFix(adapter, target, manifestValidation, json));
|
|
94
|
+
} finally {
|
|
95
|
+
lock.release();
|
|
96
|
+
}
|
|
57
97
|
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
const nextActions = doctorFixNextActions(failed, result2.fixture);
|
|
99
|
+
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
100
|
+
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions }, null, 2));
|
|
101
|
+
else {
|
|
102
|
+
console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
103
|
+
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
104
|
+
}
|
|
105
|
+
return status === "pass" ? 0 : 1;
|
|
61
106
|
}
|
|
62
107
|
const result = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
63
108
|
let runtime;
|
|
109
|
+
let runtimeProbeError;
|
|
64
110
|
try {
|
|
65
111
|
const surface = getAdapterSurface(adapter);
|
|
66
112
|
surface.resolveSlotPorts(target);
|
|
@@ -72,20 +118,43 @@ async function handleDoctor({ options }) {
|
|
|
72
118
|
}
|
|
73
119
|
applyDoctorRuntimePorts(options);
|
|
74
120
|
runtime = await surface.runtimeStatus(target);
|
|
75
|
-
} catch {
|
|
121
|
+
} catch (error) {
|
|
122
|
+
runtimeProbeError = error instanceof Error ? error.message : String(error);
|
|
76
123
|
}
|
|
124
|
+
const runtimeCheck = doctorRuntimeCheck(runtime, runtimeProbeError, expectLive);
|
|
125
|
+
const checks = [...result.checks, runtimeCheck];
|
|
126
|
+
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
127
|
+
const doctorResult = {
|
|
128
|
+
...result,
|
|
129
|
+
status: requiredChecks.status,
|
|
130
|
+
checks,
|
|
131
|
+
requiredChecks
|
|
132
|
+
};
|
|
77
133
|
const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
|
|
78
134
|
const capture = captureHelperHealth();
|
|
79
135
|
const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
|
|
80
136
|
const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
|
|
81
137
|
const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
|
|
82
138
|
const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
|
|
83
|
-
if (expectLive)
|
|
84
|
-
|
|
139
|
+
if (expectLive) {
|
|
140
|
+
return emitExpectLive(
|
|
141
|
+
adapter,
|
|
142
|
+
target,
|
|
143
|
+
runtime,
|
|
144
|
+
doctorResult,
|
|
145
|
+
orphanMetros,
|
|
146
|
+
capture,
|
|
147
|
+
devices,
|
|
148
|
+
additionalReachableDevices,
|
|
149
|
+
json,
|
|
150
|
+
printReady
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
|
|
85
154
|
else {
|
|
86
155
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
87
156
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
88
|
-
console.log(`${out(
|
|
157
|
+
console.log(`${out(doctorResult.status === "pass" ? "ok" : "err", doctorResult.status)} ${out("bold", adapter)} ${doctorResult.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
|
|
89
158
|
if (runtime) {
|
|
90
159
|
const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
|
|
91
160
|
const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
|
|
@@ -96,8 +165,16 @@ async function handleDoctor({ options }) {
|
|
|
96
165
|
`${out("label", "runtime:")} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out("dim", `(${runtime.reasonCode})`)}` : ""} deps=${out(depsStyle, runtime.deps ?? "unknown")}${devServer}`
|
|
97
166
|
);
|
|
98
167
|
for (const reason of runtime.reasons) console.log(` ${out("dim", reason)}`);
|
|
168
|
+
} else if (runtimeProbeError) {
|
|
169
|
+
console.log(`${out("err", "runtime probe failed:")} ${out("dim", runtimeProbeError)}`);
|
|
99
170
|
}
|
|
100
171
|
console.log(renderRuntimeContext(result.runtimeContext));
|
|
172
|
+
if (adapter === "mobile") {
|
|
173
|
+
const perps = mobilePerpsEnvironment(target);
|
|
174
|
+
console.log(
|
|
175
|
+
`mobile-perps: ${perps.status} (${perps.file}; MM_PERPS_ENABLED=${perps.perpsEnabled.value ?? "missing"}; OVERRIDE_REMOTE_FEATURE_FLAGS=${perps.remoteFeatureFlagOverride.value ?? "missing"})`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
101
178
|
if (orphanMetros.length > 0) {
|
|
102
179
|
console.log(
|
|
103
180
|
`${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
|
|
@@ -117,12 +194,62 @@ async function handleDoctor({ options }) {
|
|
|
117
194
|
if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
|
|
118
195
|
}
|
|
119
196
|
}
|
|
120
|
-
return
|
|
197
|
+
return doctorResult.status === "pass" ? 0 : 1;
|
|
198
|
+
}
|
|
199
|
+
function doctorRuntimeCheck(runtime, error, expectLive) {
|
|
200
|
+
if (error) {
|
|
201
|
+
return {
|
|
202
|
+
id: "runtime",
|
|
203
|
+
status: "fail",
|
|
204
|
+
required: true,
|
|
205
|
+
message: "Runtime readiness probe failed.",
|
|
206
|
+
detail: error
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const ready = runtime?.decision === "ready";
|
|
210
|
+
return {
|
|
211
|
+
id: "runtime",
|
|
212
|
+
status: ready ? "pass" : "fail",
|
|
213
|
+
required: expectLive,
|
|
214
|
+
message: ready ? "Runtime is ready." : `Runtime is not ready${runtime?.reasonCode ? ` (${runtime.reasonCode})` : "."}`,
|
|
215
|
+
detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function doctorFixNextActions(failed, fixture) {
|
|
219
|
+
const actions = [];
|
|
220
|
+
if (failed.includes("wallet-fixture")) {
|
|
221
|
+
const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
|
|
222
|
+
actions.push(`1) mm-harness fixtures init --from <path>${force} # existing team/test fixture`);
|
|
223
|
+
actions.push(`2) mm-harness fixtures init --dev${force} # disposable public test wallet; never real funds`);
|
|
224
|
+
}
|
|
225
|
+
if (failed.includes("runtime-context")) actions.push("inspect temp/recipe/runtime/agentic-runtime.json, then retry mm-harness doctor --fix");
|
|
226
|
+
if (failed.includes("overlay")) actions.push("mm-harness install --target .");
|
|
227
|
+
return actions;
|
|
228
|
+
}
|
|
229
|
+
function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
230
|
+
const out = (style, text) => color(style, text, { stream: process.stderr });
|
|
231
|
+
const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
|
|
232
|
+
console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
233
|
+
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
234
|
+
console.error(` Next: ${getAdapterSurface(adapter).hints.relaunch}`);
|
|
121
235
|
}
|
|
122
|
-
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json) {
|
|
236
|
+
function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
|
|
123
237
|
const live = runtime?.decision === "ready";
|
|
238
|
+
if (printReady) {
|
|
239
|
+
if (live) {
|
|
240
|
+
const indicator = farmslotReadyIndicator(adapter, devices);
|
|
241
|
+
if (indicator) {
|
|
242
|
+
console.log(indicator);
|
|
243
|
+
return EXIT.ok;
|
|
244
|
+
}
|
|
245
|
+
emitExpectLiveVerdict(adapter, target, runtime, "not-ready");
|
|
246
|
+
return EXIT.runtime;
|
|
247
|
+
}
|
|
248
|
+
emitExpectLiveVerdict(adapter, target, runtime, "not-live");
|
|
249
|
+
return EXIT.runtime;
|
|
250
|
+
}
|
|
124
251
|
if (json) {
|
|
125
|
-
console.log(JSON.stringify({ ...result, runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
|
|
252
|
+
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
|
|
126
253
|
return live ? EXIT.ok : EXIT.runtime;
|
|
127
254
|
}
|
|
128
255
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -130,10 +257,7 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
130
257
|
console.log(`${out("ok", "live")} ${out("bold", adapter)} runtime ready`);
|
|
131
258
|
return EXIT.ok;
|
|
132
259
|
}
|
|
133
|
-
|
|
134
|
-
console.error(`${out("err", "not-live")} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
|
|
135
|
-
for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
|
|
136
|
-
console.error(` Next: ${getAdapterSurface(adapter).hints.relaunch}`);
|
|
260
|
+
emitExpectLiveVerdict(adapter, target, runtime, "not-live");
|
|
137
261
|
return EXIT.runtime;
|
|
138
262
|
}
|
|
139
263
|
function captureHelperHealth() {
|
|
@@ -168,12 +292,103 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
|
|
|
168
292
|
return { fixed, failed };
|
|
169
293
|
}
|
|
170
294
|
if (Number(manifestValidation.summary?.errors ?? 0) > 0) failed.push("manifest");
|
|
295
|
+
try {
|
|
296
|
+
const surface = getAdapterSurface(adapter);
|
|
297
|
+
surface.resolveSlotPorts(target);
|
|
298
|
+
const runtimeContext = ensureRuntimeContext(target, adapter);
|
|
299
|
+
if (runtimeContext.created) fixed.push("runtime-context");
|
|
300
|
+
} catch {
|
|
301
|
+
failed.push("runtime-context");
|
|
302
|
+
}
|
|
303
|
+
if (adapter === "extension") {
|
|
304
|
+
try {
|
|
305
|
+
if (ensureExtensionPerpsBuildFlag(target)) fixed.push("extension-perps-build");
|
|
306
|
+
} catch {
|
|
307
|
+
failed.push("extension-perps-build");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (adapter === "mobile") {
|
|
311
|
+
try {
|
|
312
|
+
if (ensureMobilePerpsEnvironment(target).length > 0) fixed.push("mobile-perps-env");
|
|
313
|
+
} catch {
|
|
314
|
+
failed.push("mobile-perps-env");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
171
317
|
const state = newHealState();
|
|
172
318
|
const ensured = await ensureOverlay(adapter, target, "infra-only", state, json);
|
|
173
319
|
if (!ensured.ok) failed.push("overlay");
|
|
174
320
|
else if (state.mutations.length > 0) fixed.push("overlay");
|
|
321
|
+
let fixture = fixtureSummary(target, adapter);
|
|
322
|
+
if (fixture.status !== "ready") {
|
|
323
|
+
const syncFixture = path.join(runnerDir, "adapters/shared/sync-wallet-fixture.sh");
|
|
324
|
+
try {
|
|
325
|
+
execFileSync("bash", [syncFixture, "--target", target], {
|
|
326
|
+
encoding: "utf8",
|
|
327
|
+
stdio: json ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
328
|
+
timeout: 3e4
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
fixture = fixtureSummary(target, adapter);
|
|
333
|
+
if (fixture.status === "ready") fixed.push("wallet-fixture");
|
|
334
|
+
else failed.push("wallet-fixture");
|
|
335
|
+
}
|
|
175
336
|
return { fixed, failed };
|
|
176
337
|
}
|
|
338
|
+
function ensureExtensionPerpsBuildFlag(target) {
|
|
339
|
+
const configPath = path.join(target, ".metamaskrc");
|
|
340
|
+
const templatePath = path.join(target, ".metamaskrc.dist");
|
|
341
|
+
const config = readTextSnapshot(configPath);
|
|
342
|
+
const template = config.kind === "file" ? null : readTextSnapshot(templatePath);
|
|
343
|
+
const source = config.kind === "file" ? config : template?.kind === "file" ? template : config.kind === "symlink" ? { kind: "file", text: "", mode: 438 } : null;
|
|
344
|
+
if (!source) return false;
|
|
345
|
+
const before = source.text;
|
|
346
|
+
if (/^\s*PERPS_ENABLED\s*=\s*(['"]?)true\1\s*$/mu.test(before)) return false;
|
|
347
|
+
const line = "PERPS_ENABLED='true'";
|
|
348
|
+
const after = /^\s*PERPS_ENABLED\s*=.*$/mu.test(before) ? before.replace(/^\s*PERPS_ENABLED\s*=.*$/mu, line) : `${before.replace(/\s*$/u, "")}
|
|
349
|
+
${line}
|
|
350
|
+
`;
|
|
351
|
+
const temporary = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
352
|
+
let descriptor = null;
|
|
353
|
+
try {
|
|
354
|
+
const mode = config.kind === "file" ? config.mode : 438;
|
|
355
|
+
descriptor = fs.openSync(
|
|
356
|
+
temporary,
|
|
357
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
358
|
+
mode
|
|
359
|
+
);
|
|
360
|
+
fs.writeFileSync(descriptor, after);
|
|
361
|
+
if (config.kind === "file") fs.fchmodSync(descriptor, config.mode);
|
|
362
|
+
fs.closeSync(descriptor);
|
|
363
|
+
descriptor = null;
|
|
364
|
+
fs.renameSync(temporary, configPath);
|
|
365
|
+
} finally {
|
|
366
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
367
|
+
fs.rmSync(temporary, { force: true });
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
function readTextSnapshot(file) {
|
|
372
|
+
let descriptor;
|
|
373
|
+
try {
|
|
374
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
const code = error.code;
|
|
377
|
+
if (code === "ENOENT") return { kind: "missing" };
|
|
378
|
+
if (code === "ELOOP") return { kind: "symlink" };
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
return {
|
|
383
|
+
kind: "file",
|
|
384
|
+
text: fs.readFileSync(descriptor, "utf8"),
|
|
385
|
+
mode: fs.fstatSync(descriptor).mode & 511
|
|
386
|
+
};
|
|
387
|
+
} finally {
|
|
388
|
+
fs.closeSync(descriptor);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
177
391
|
export {
|
|
392
|
+
ensureExtensionPerpsBuildFlag,
|
|
178
393
|
handleDoctor
|
|
179
394
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function selectedDevice(devices) {
|
|
2
|
+
return devices.find((d) => d.selected) ?? devices[0];
|
|
3
|
+
}
|
|
4
|
+
function mobileWalletReady(device) {
|
|
5
|
+
if (device.liveState === "no-bridge" || device.liveState === "bridge-absent") return false;
|
|
6
|
+
return device.walletState === "unlocked";
|
|
7
|
+
}
|
|
8
|
+
function farmslotReadyIndicator(adapter, devices) {
|
|
9
|
+
switch (adapter) {
|
|
10
|
+
case "extension":
|
|
11
|
+
return "OK";
|
|
12
|
+
case "core":
|
|
13
|
+
return "ready";
|
|
14
|
+
case "mobile": {
|
|
15
|
+
const device = selectedDevice(devices);
|
|
16
|
+
if (!device) return "";
|
|
17
|
+
return mobileWalletReady(device) ? "OK" : "";
|
|
18
|
+
}
|
|
19
|
+
default:
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
farmslotReadyIndicator
|
|
25
|
+
};
|
|
@@ -1,38 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
5
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
6
|
+
import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
|
|
7
|
+
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
5
8
|
import { applyDeviceTargeting } from "./device-target.js";
|
|
6
|
-
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
7
|
-
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
|
|
10
|
+
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
|
|
11
|
+
const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
|
|
12
|
+
async function handleFixtures(argv, deps) {
|
|
13
|
+
const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
14
|
+
const json = flag(options, "json");
|
|
15
|
+
const target = targetOf(options);
|
|
16
|
+
if (!fs.existsSync(target)) return handleFixturesLocked(argv, deps);
|
|
17
|
+
const lock = acquireCheckoutLock(target, "fixtures");
|
|
18
|
+
if ("message" in lock) {
|
|
19
|
+
return checkoutBusyOut(json, "fixtures", lock.message, lock.path);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return await handleFixturesLocked(argv, deps);
|
|
23
|
+
} finally {
|
|
24
|
+
lock.release();
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
|
-
function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
27
|
+
function ensureFixturePublicationParent(target) {
|
|
28
|
+
const repoRoot = fs.realpathSync(target);
|
|
29
|
+
const parent = path.dirname(walletFixturePath(repoRoot));
|
|
30
|
+
const relative = path.relative(repoRoot, parent);
|
|
31
|
+
if (!relative || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
32
|
+
return `wallet fixture runtime path escapes the checkout: ${parent}`;
|
|
33
|
+
}
|
|
34
|
+
let current = repoRoot;
|
|
35
|
+
for (const part of relative.split(path.sep)) {
|
|
36
|
+
current = path.join(current, part);
|
|
37
|
+
try {
|
|
38
|
+
const entry = fs.lstatSync(current);
|
|
39
|
+
if (entry.isSymbolicLink()) {
|
|
40
|
+
return `wallet fixture runtime path must not contain symlinks: ${current}`;
|
|
41
|
+
}
|
|
42
|
+
if (!entry.isDirectory()) {
|
|
43
|
+
return `wallet fixture runtime path component is not a directory: ${current}`;
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
fs.mkdirSync(current);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return fs.realpathSync(parent) === parent ? null : `wallet fixture runtime path escapes the checkout: ${parent}`;
|
|
29
51
|
}
|
|
30
|
-
async function
|
|
52
|
+
async function handleFixturesLocked(argv, deps) {
|
|
31
53
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
32
54
|
const json = flag(options, "json");
|
|
33
55
|
const sub = positional[0];
|
|
34
|
-
if (sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
35
|
-
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <sync|set|generate|finalize>", "mm-harness fixtures
|
|
56
|
+
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
57
|
+
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
36
58
|
}
|
|
37
59
|
const target = targetOf(options);
|
|
38
60
|
const adapter = resolveAdapter(options, target);
|
|
@@ -47,8 +69,18 @@ async function handleFixtures(argv, deps) {
|
|
|
47
69
|
}
|
|
48
70
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
49
71
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
50
|
-
if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
|
|
51
72
|
const canonicalFixture = walletFixturePath(target);
|
|
73
|
+
if (sub === "init") return fixturesInit(adapter, target, options, json);
|
|
74
|
+
if (surface.headless && sub === "set") {
|
|
75
|
+
const fixture = fixtureSummary(target, adapter);
|
|
76
|
+
if (fixture.status !== "ready") {
|
|
77
|
+
return usageOut(json, "fixtures", `core wallet fixture is ${String(fixture.status)} at ${canonicalFixture}.`, "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
78
|
+
}
|
|
79
|
+
const message = `Core reads the canonical signing fixture directly: ${canonicalFixture}`;
|
|
80
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "fixtures", action: "set", adapter, fixture: canonicalFixture, status: "pass", exitCode: EXIT.ok, message }, null, 2));
|
|
81
|
+
else console.error(message);
|
|
82
|
+
return EXIT.ok;
|
|
83
|
+
}
|
|
52
84
|
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
|
|
53
85
|
if (sub === "sync") {
|
|
54
86
|
const exitCode = fixturesSync(adapter, target, json);
|
|
@@ -67,25 +99,29 @@ async function handleFixtures(argv, deps) {
|
|
|
67
99
|
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
68
100
|
`);
|
|
69
101
|
let status;
|
|
102
|
+
let mobileSetupResult;
|
|
70
103
|
if (adapter === "mobile") {
|
|
71
104
|
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
72
105
|
const previousAppRoot = process.env.APP_ROOT;
|
|
73
106
|
process.env.APP_ROOT = target;
|
|
74
107
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
|
|
109
|
+
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
|
|
110
|
+
mobileSetupResult = await spawnScriptStreaming(
|
|
111
|
+
setupWalletSh,
|
|
112
|
+
["--fixture", fixturePath],
|
|
113
|
+
target,
|
|
114
|
+
{
|
|
115
|
+
env: {
|
|
116
|
+
APP_ROOT: target,
|
|
117
|
+
WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
|
|
118
|
+
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
|
|
119
|
+
},
|
|
120
|
+
timeoutMs
|
|
84
121
|
}
|
|
85
|
-
|
|
86
|
-
status =
|
|
122
|
+
);
|
|
123
|
+
status = mobileSetupResult.status === 0 ? "pass" : "fail";
|
|
87
124
|
} finally {
|
|
88
|
-
delete process.env["RECIPE_SETUP_WALLET_RETRIED"];
|
|
89
125
|
if (previousAppRoot === void 0) delete process.env.APP_ROOT;
|
|
90
126
|
else process.env.APP_ROOT = previousAppRoot;
|
|
91
127
|
}
|
|
@@ -129,8 +165,8 @@ async function handleFixtures(argv, deps) {
|
|
|
129
165
|
exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
130
166
|
message: teaching,
|
|
131
167
|
error: status === "fail" ? {
|
|
132
|
-
code: "SETUP_WALLET_FAILED",
|
|
133
|
-
message: "wallet fixture setup failed"
|
|
168
|
+
code: mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
169
|
+
message: mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
134
170
|
userAction: retryHint
|
|
135
171
|
} : null
|
|
136
172
|
},
|
|
@@ -262,7 +298,9 @@ async function fixturesFinalize(adapter, target, options, json) {
|
|
|
262
298
|
if (outPath) leafArgs.push("--out", outPath);
|
|
263
299
|
process.stderr.write(`\u2192 fixtures finalize extension \u2014 seeding account labels via CDP on port ${cdpPort} (can take ~30s)\u2026
|
|
264
300
|
`);
|
|
265
|
-
const result = await spawnScriptStreaming(process.execPath, leafArgs, target
|
|
301
|
+
const result = await spawnScriptStreaming(process.execPath, leafArgs, target, {
|
|
302
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
303
|
+
});
|
|
266
304
|
const status = result.status === 0 ? "pass" : "fail";
|
|
267
305
|
const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
268
306
|
const retry = `mm-harness fixtures finalize --fixture ${fixturePath} --state ${statePath} --cdp-port ${cdpPort} --extension-dir ${extensionDirPath}`;
|
|
@@ -316,7 +354,104 @@ function fixturesSync(adapter, target, json) {
|
|
|
316
354
|
const syncArgs = ["--target", target];
|
|
317
355
|
if (process.env.CDP_PORT) syncArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
318
356
|
if (process.env.RECIPE_SLOT_ID) syncArgs.push("--slot-id", process.env.RECIPE_SLOT_ID);
|
|
319
|
-
spawnScript(syncFixtureSh, syncArgs, target, json);
|
|
357
|
+
const syncResult = spawnScript(syncFixtureSh, syncArgs, target, json);
|
|
358
|
+
if (syncResult.status !== 0 || fixtureSummary(target, adapter).status !== "ready") {
|
|
359
|
+
if (!json) console.error("\u2717 no usable wallet fixture source was found.\n Next: mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
360
|
+
return EXIT.runtime;
|
|
361
|
+
}
|
|
362
|
+
return EXIT.ok;
|
|
363
|
+
}
|
|
364
|
+
function fixturesInit(adapter, target, options, json) {
|
|
365
|
+
const canonical = walletFixturePath(target);
|
|
366
|
+
const containmentError = ensureFixturePublicationParent(target);
|
|
367
|
+
if (containmentError) {
|
|
368
|
+
return usageOut(json, "fixtures", containmentError, "replace the runtime path with checkout-contained directories, then retry");
|
|
369
|
+
}
|
|
370
|
+
const source = str(options, "from");
|
|
371
|
+
const dev = flag(options, "dev");
|
|
372
|
+
const force = flag(options, "force");
|
|
373
|
+
if (!source && !dev) {
|
|
374
|
+
return usageOut(
|
|
375
|
+
json,
|
|
376
|
+
"fixtures",
|
|
377
|
+
"choose a wallet fixture source: 1) --from <path> for an existing team/test fixture; 2) --dev for a disposable public test wallet.",
|
|
378
|
+
"mm-harness fixtures init --from <path> or mm-harness fixtures init --dev"
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
if (source && dev) {
|
|
382
|
+
return usageOut(json, "fixtures", "--from and --dev are mutually exclusive.", "choose exactly one fixture source");
|
|
383
|
+
}
|
|
384
|
+
if (!force) {
|
|
385
|
+
try {
|
|
386
|
+
fs.lstatSync(canonical);
|
|
387
|
+
return usageOut(json, "fixtures", `wallet fixture already exists: ${canonical}`, "re-run with --force only when replacement is intentional");
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (error.code !== "ENOENT") throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
393
|
+
let fixtureBytes;
|
|
394
|
+
if (source) {
|
|
395
|
+
const resolvedSource = path.resolve(source);
|
|
396
|
+
try {
|
|
397
|
+
fixtureBytes = fs.readFileSync(resolvedSource);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error.code === "ENOENT") {
|
|
400
|
+
return usageOut(json, "fixtures", `wallet fixture source does not exist: ${resolvedSource}`, "pass an existing JSON file to --from");
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
const fixture = {
|
|
406
|
+
password: "metamask",
|
|
407
|
+
accounts: [{
|
|
408
|
+
type: "mnemonic",
|
|
409
|
+
value: "test test test test test test test test test test test junk",
|
|
410
|
+
name: "dev1",
|
|
411
|
+
count: 1,
|
|
412
|
+
names: ["dev1"]
|
|
413
|
+
}],
|
|
414
|
+
settings: { skipPerpsTutorial: true, autoLockNever: true }
|
|
415
|
+
};
|
|
416
|
+
fixtureBytes = `${JSON.stringify(fixture, null, 2)}
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
const temporary = `${canonical}.${process.pid}.${randomUUID()}.tmp`;
|
|
420
|
+
let descriptor = null;
|
|
421
|
+
try {
|
|
422
|
+
descriptor = fs.openSync(
|
|
423
|
+
temporary,
|
|
424
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
425
|
+
384
|
|
426
|
+
);
|
|
427
|
+
fs.writeFileSync(descriptor, fixtureBytes);
|
|
428
|
+
fs.fchmodSync(descriptor, 384);
|
|
429
|
+
fs.closeSync(descriptor);
|
|
430
|
+
descriptor = null;
|
|
431
|
+
const fixture = fixtureFileSummary(temporary, adapter, canonical);
|
|
432
|
+
if (fixture.status !== "ready") {
|
|
433
|
+
return usageOut(json, "fixtures", "wallet fixture is incomplete or invalid for this adapter.", "provide accounts and, for Mobile/Extension, a non-empty password");
|
|
434
|
+
}
|
|
435
|
+
if (force) {
|
|
436
|
+
fs.renameSync(temporary, canonical);
|
|
437
|
+
} else {
|
|
438
|
+
try {
|
|
439
|
+
fs.linkSync(temporary, canonical);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
if (error.code === "EEXIST") {
|
|
442
|
+
return usageOut(json, "fixtures", `wallet fixture already exists: ${canonical}`, "re-run with --force only when replacement is intentional");
|
|
443
|
+
}
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} finally {
|
|
448
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
449
|
+
fs.rmSync(temporary, { force: true });
|
|
450
|
+
}
|
|
451
|
+
const warning = dev ? "Disposable public test wallet created; never use or fund it with real assets." : "Wallet fixture copied.";
|
|
452
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "fixtures", action: "init", adapter, fixture: canonical, status: "pass", warning, exitCode: EXIT.ok }, null, 2));
|
|
453
|
+
else console.error(`${warning}
|
|
454
|
+
Wallet fixture: ${canonical}`);
|
|
320
455
|
return EXIT.ok;
|
|
321
456
|
}
|
|
322
457
|
export {
|