@deeeed/metamask-harness 0.14.7 → 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 +81 -6
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +4 -2
- package/adapters/extension/inject.mjs +0 -0
- package/adapters/extension/launch-browser.cjs +73 -34
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/live.sh +21 -10
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +3 -3
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +10 -4
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +2 -2
- package/adapters/extension/stop-viewers.sh +0 -0
- 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/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +6 -4
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -0
- package/adapters/mobile/launch-metro.cjs +74 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +4 -4
- package/adapters/mobile/open-device.sh +3 -1
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- 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/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +29 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +35 -4
- package/adapters/shared/open-log-window.sh +1 -1
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +72 -1
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +21 -17
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- 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 +3 -0
- 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.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 +187 -12
- 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/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 +37 -9
- 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 +5 -5
- package/scripts/completions.sh +7 -4
- package/scripts/install-completions.sh +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
INSTALL_MARKERS,
|
|
8
8
|
recordDepsBaseline
|
|
9
9
|
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
10
|
-
import { recipeHarnessPath, recipeWatchLogCandidates } from "../../paths.js";
|
|
10
|
+
import { recipeHarnessPath, recipeRuntimeDir, recipeWatchLogCandidates } from "../../paths.js";
|
|
11
11
|
const WEBPACK_DIRECT_INPUTS = ["package.json", "yarn.lock", ".yarnrc.yml", ".tool-versions"];
|
|
12
12
|
const WEBPACK_RECURSIVE_INPUTS = ["development/webpack"];
|
|
13
13
|
const WEBPACK_CACHE_DIR = "node_modules/.cache/webpack";
|
|
@@ -152,6 +152,80 @@ function latestDistMtime(target) {
|
|
|
152
152
|
walk2(distDir);
|
|
153
153
|
return latest;
|
|
154
154
|
}
|
|
155
|
+
function runtimeDistCheck(target) {
|
|
156
|
+
const dist = path.join(target, "dist/chrome");
|
|
157
|
+
const runtimeDist = path.join(
|
|
158
|
+
target,
|
|
159
|
+
recipeRuntimeDir(),
|
|
160
|
+
process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"
|
|
161
|
+
);
|
|
162
|
+
if (!fs.existsSync(path.join(runtimeDist, "manifest.json"))) return { status: "missing" };
|
|
163
|
+
let output;
|
|
164
|
+
try {
|
|
165
|
+
output = execFileSync("rsync", [
|
|
166
|
+
"-rni",
|
|
167
|
+
"--exclude",
|
|
168
|
+
"_metadata",
|
|
169
|
+
"--out-format=%n",
|
|
170
|
+
`${dist}${path.sep}`,
|
|
171
|
+
`${runtimeDist}${path.sep}`
|
|
172
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error.code === "ENOENT") {
|
|
175
|
+
try {
|
|
176
|
+
const modified2 = filesystemSnapshotDiff(dist, runtimeDist).slice(0, 10);
|
|
177
|
+
return modified2.length > 0 ? { status: "stale", comparison: "filesystem-quick", modified: modified2 } : { status: "fresh", comparison: "filesystem-quick" };
|
|
178
|
+
} catch (fallbackError) {
|
|
179
|
+
return {
|
|
180
|
+
status: "unknown",
|
|
181
|
+
error: `snapshot comparison failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const status = error.status;
|
|
186
|
+
return {
|
|
187
|
+
status: "unknown",
|
|
188
|
+
error: `rsync snapshot comparison failed${typeof status === "number" ? ` with exit ${status}` : ""}`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 10);
|
|
192
|
+
return modified.length > 0 ? { status: "stale", comparison: "rsync-quick", modified } : { status: "fresh", comparison: "rsync-quick" };
|
|
193
|
+
}
|
|
194
|
+
function filesystemSnapshotDiff(source, snapshot) {
|
|
195
|
+
const sourceEntries = snapshotEntries(source);
|
|
196
|
+
const snapshotEntriesByPath = snapshotEntries(snapshot);
|
|
197
|
+
const paths = [.../* @__PURE__ */ new Set([...sourceEntries.keys(), ...snapshotEntriesByPath.keys()])].sort();
|
|
198
|
+
return paths.filter((relative) => {
|
|
199
|
+
const left = sourceEntries.get(relative);
|
|
200
|
+
const right = snapshotEntriesByPath.get(relative);
|
|
201
|
+
return left === void 0 || right === void 0 || JSON.stringify(left) !== JSON.stringify(right);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function snapshotEntries(root) {
|
|
205
|
+
const entries = /* @__PURE__ */ new Map();
|
|
206
|
+
const walk2 = (directory) => {
|
|
207
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
208
|
+
if (entry.name === "_metadata") continue;
|
|
209
|
+
const absolute = path.join(directory, entry.name);
|
|
210
|
+
const relative = path.relative(root, absolute);
|
|
211
|
+
const stat = fs.lstatSync(absolute);
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
entries.set(relative, { type: "directory" });
|
|
214
|
+
walk2(absolute);
|
|
215
|
+
} else if (entry.isSymbolicLink()) {
|
|
216
|
+
entries.set(relative, { type: "symlink", link: fs.readlinkSync(absolute) });
|
|
217
|
+
} else if (entry.isFile()) {
|
|
218
|
+
entries.set(relative, {
|
|
219
|
+
type: "file",
|
|
220
|
+
size: stat.size,
|
|
221
|
+
mtimeSeconds: Math.floor(stat.mtimeMs / 1e3)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk2(root);
|
|
227
|
+
return entries;
|
|
228
|
+
}
|
|
155
229
|
function distCheck(target) {
|
|
156
230
|
const manifestPath = path.join(target, "dist/chrome/manifest.json");
|
|
157
231
|
if (!fs.existsSync(manifestPath)) return { status: "no-build" };
|
|
@@ -206,8 +280,9 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
206
280
|
const webpackCache = webpackCacheCheck(resolved);
|
|
207
281
|
const buildLog = buildLogCheck(resolved, options.watchLog);
|
|
208
282
|
const dist = distCheck(resolved);
|
|
283
|
+
const runtimeDist = runtimeDistCheck(resolved);
|
|
209
284
|
const cdp = await cdpCheck(resolved, options.cdpPort, options.pageMode);
|
|
210
|
-
const checks = { deps, webpackCache, buildLog, dist, cdp };
|
|
285
|
+
const checks = { deps, webpackCache, buildLog, dist, runtimeDist, cdp };
|
|
211
286
|
const install = [{ id: "yarn-install", argv: ["yarn", "install", "--immutable"], cwd: resolved }];
|
|
212
287
|
const relaunch = [{ id: "relaunch-browser" }];
|
|
213
288
|
const cacheStale = buildLog.reason === "stale-cache";
|
|
@@ -259,6 +334,20 @@ async function decideExtensionReadiness(target, options = {}) {
|
|
|
259
334
|
reasons: [dist.reason === "uncommitted-source" ? `Uncommitted source since the build (${dist.modified?.length ?? 0} file(s)); rebuild.` : `dist git id ${dist.distGitId} != HEAD ${dist.head}; rebuild.`],
|
|
260
335
|
actions: buildAction(resolved, false)
|
|
261
336
|
},
|
|
337
|
+
{
|
|
338
|
+
when: runtimeDist.status === "unknown",
|
|
339
|
+
decision: "blocked",
|
|
340
|
+
reasonCode: "runtime-dist-check-failed",
|
|
341
|
+
reasons: [runtimeDist.error ?? "The loaded runtime snapshot could not be compared with dist/chrome."],
|
|
342
|
+
actions: []
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
when: runtimeDist.status !== "fresh",
|
|
346
|
+
decision: "relaunch",
|
|
347
|
+
reasonCode: runtimeDist.status === "missing" ? "runtime-dist-missing" : "runtime-dist-stale",
|
|
348
|
+
reasons: [runtimeDist.status === "missing" ? "The loaded runtime snapshot is missing; launch before proving behavior." : `The loaded runtime snapshot differs from dist/chrome (${runtimeDist.modified?.length ?? 0} file(s)); reload before proving behavior.`],
|
|
349
|
+
actions: relaunch
|
|
350
|
+
},
|
|
262
351
|
{
|
|
263
352
|
when: cdp.status === "pass",
|
|
264
353
|
decision: "ready",
|
|
@@ -305,5 +394,6 @@ function buildAction(target, clean) {
|
|
|
305
394
|
export {
|
|
306
395
|
decideExtensionReadiness,
|
|
307
396
|
isExtensionDistStale,
|
|
308
|
-
recordReadinessBaseline
|
|
397
|
+
recordReadinessBaseline,
|
|
398
|
+
runtimeDistCheck
|
|
309
399
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isExtensionWatcherLive,
|
|
6
|
+
resolveExtensionSlotPorts,
|
|
7
|
+
stopExtensionRuntime
|
|
8
|
+
} from "../slot-ports.js";
|
|
5
9
|
import { decideExtensionReadiness } from "./runtime-decision.js";
|
|
6
10
|
function watcherStatus(buildLog) {
|
|
7
11
|
if (buildLog === "ok") return "up";
|
|
@@ -17,12 +21,16 @@ const extensionSurface = {
|
|
|
17
21
|
async runtimeStatus(target) {
|
|
18
22
|
const cdpPort = process.env.CDP_PORT ? parseInt(process.env.CDP_PORT, 10) : void 0;
|
|
19
23
|
const report = await decideExtensionReadiness(target, { cdpPort, pageMode: "home-or-sidepanel" });
|
|
24
|
+
const watcherLive = isExtensionWatcherLive(target);
|
|
20
25
|
return {
|
|
21
26
|
decision: report.decision,
|
|
22
27
|
reasonCode: report.reasonCode,
|
|
23
28
|
reasons: report.reasons,
|
|
24
29
|
deps: report.checks.deps.status,
|
|
25
|
-
devServer: {
|
|
30
|
+
devServer: {
|
|
31
|
+
label: "webpack",
|
|
32
|
+
status: watcherLive ? watcherStatus(report.checks.buildLog.status) : "down"
|
|
33
|
+
}
|
|
26
34
|
};
|
|
27
35
|
},
|
|
28
36
|
runwayProvision: {
|
|
@@ -44,13 +52,13 @@ const extensionSurface = {
|
|
|
44
52
|
},
|
|
45
53
|
devServer: {
|
|
46
54
|
label: "webpack",
|
|
47
|
-
describe: () => "
|
|
55
|
+
describe: () => "Extension runtime",
|
|
48
56
|
stop(target) {
|
|
49
|
-
const
|
|
57
|
+
const stopped = stopExtensionRuntime(target);
|
|
50
58
|
const stopViewers = path.join(runnerDir, "adapters/extension/stop-viewers.sh");
|
|
51
59
|
spawnSync("bash", [stopViewers, "--target", target], { stdio: "ignore", timeout: 5e3 });
|
|
52
|
-
const summary = signalled > 0 ? `stopped
|
|
53
|
-
return { kind: "stopped", status: 0, summary, signalled };
|
|
60
|
+
const summary = stopped.signalled > 0 || stopped.removedMarkers > 0 ? `stopped Extension runtime (${stopped.signalled} process${stopped.signalled === 1 ? "" : "es"}, ${stopped.removedMarkers} stale marker${stopped.removedMarkers === 1 ? "" : "s"}) for ${target}` : `Extension runtime not running for ${target} \u2014 nothing to stop`;
|
|
61
|
+
return { kind: "stopped", status: 0, summary, signalled: stopped.signalled };
|
|
54
62
|
}
|
|
55
63
|
},
|
|
56
64
|
logSources(target) {
|
|
@@ -60,6 +68,9 @@ const extensionSurface = {
|
|
|
60
68
|
{ label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") }
|
|
61
69
|
];
|
|
62
70
|
},
|
|
71
|
+
appLogSource(target) {
|
|
72
|
+
return { label: "app", path: recipeRuntimePath(target, "extension-console.log") };
|
|
73
|
+
},
|
|
63
74
|
hints: {
|
|
64
75
|
launch: "mm-harness launch",
|
|
65
76
|
relaunch: "mm-harness launch --build"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { recipeRuntimePath } from "../../paths.js";
|
|
5
|
+
const ENV_INPUTS = [".js.env", ".env", ".env.local"];
|
|
6
|
+
const BASELINE_FILE = "metro-env-baseline.json";
|
|
7
|
+
function mobileMetroEnvFingerprint(target) {
|
|
8
|
+
const hash = createHash("sha256");
|
|
9
|
+
for (const relative of ENV_INPUTS) {
|
|
10
|
+
const absolute = path.join(target, relative);
|
|
11
|
+
hash.update(`${relative}\0`);
|
|
12
|
+
if (fs.existsSync(absolute)) hash.update(fs.readFileSync(absolute));
|
|
13
|
+
else hash.update("<absent>");
|
|
14
|
+
hash.update("\0");
|
|
15
|
+
}
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
}
|
|
18
|
+
function mobileMetroEnvCheck(target) {
|
|
19
|
+
const fingerprint = mobileMetroEnvFingerprint(target);
|
|
20
|
+
const marker = readBaseline(target);
|
|
21
|
+
return {
|
|
22
|
+
fingerprint,
|
|
23
|
+
status: marker === null ? "missing" : marker.fingerprint === fingerprint ? "current" : "changed"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function applyMobileMetroEnvPolicy(target, actions) {
|
|
27
|
+
if (mobileMetroEnvCheck(target).status === "current") return actions;
|
|
28
|
+
return actions.map((action) => action.id === "start-metro" && !action.argv?.includes("--clear") ? { ...action, argv: [...action.argv ?? [], "--clear"] } : action);
|
|
29
|
+
}
|
|
30
|
+
function recordMobileMetroEnvBaseline(target, expectedFingerprint) {
|
|
31
|
+
if (mobileMetroEnvFingerprint(target) !== expectedFingerprint) return false;
|
|
32
|
+
const marker = recipeRuntimePath(target, BASELINE_FILE);
|
|
33
|
+
const temporary = `${marker}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
+
const baseline = {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
fingerprint: expectedFingerprint,
|
|
37
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38
|
+
};
|
|
39
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(temporary, `${JSON.stringify(baseline, null, 2)}
|
|
42
|
+
`, { mode: 384 });
|
|
43
|
+
if (mobileMetroEnvFingerprint(target) !== expectedFingerprint) return false;
|
|
44
|
+
fs.renameSync(temporary, marker);
|
|
45
|
+
if (mobileMetroEnvFingerprint(target) !== expectedFingerprint) {
|
|
46
|
+
if (readBaseline(target)?.fingerprint === expectedFingerprint) fs.rmSync(marker, { force: true });
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
fs.rmSync(temporary, { force: true });
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
function readBaseline(target) {
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(fs.readFileSync(recipeRuntimePath(target, BASELINE_FILE), "utf8"));
|
|
57
|
+
return parsed.schemaVersion === 1 && typeof parsed.fingerprint === "string" ? parsed : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
applyMobileMetroEnvPolicy,
|
|
64
|
+
mobileMetroEnvCheck,
|
|
65
|
+
mobileMetroEnvFingerprint,
|
|
66
|
+
recordMobileMetroEnvBaseline
|
|
67
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
MM_PERPS_ENABLED: "true",
|
|
6
|
+
OVERRIDE_REMOTE_FEATURE_FLAGS: "true"
|
|
7
|
+
};
|
|
8
|
+
function mobilePerpsEnvironment(target) {
|
|
9
|
+
const file = path.join(target, ".js.env");
|
|
10
|
+
const fileExists = fs.existsSync(file);
|
|
11
|
+
const source = fileExists ? fs.readFileSync(file, "utf8") : "";
|
|
12
|
+
const perpsEnabled = readSetting(source, "MM_PERPS_ENABLED");
|
|
13
|
+
const remoteFeatureFlagOverride = readSetting(source, "OVERRIDE_REMOTE_FEATURE_FLAGS");
|
|
14
|
+
return {
|
|
15
|
+
file: ".js.env",
|
|
16
|
+
fileExists,
|
|
17
|
+
status: perpsEnabled.status === "missing" || remoteFeatureFlagOverride.status === "missing" ? "incomplete" : "ready",
|
|
18
|
+
perpsEnabled,
|
|
19
|
+
remoteFeatureFlagOverride
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function ensureMobilePerpsEnvironment(target) {
|
|
23
|
+
const current = mobilePerpsEnvironment(target);
|
|
24
|
+
const missing = Object.keys(DEFAULTS).filter(
|
|
25
|
+
(key) => settingForKey(current, key).status === "missing"
|
|
26
|
+
);
|
|
27
|
+
if (missing.length === 0) return [];
|
|
28
|
+
const file = path.join(fs.realpathSync(target), current.file);
|
|
29
|
+
const snapshot = readEnvironmentSnapshot(file);
|
|
30
|
+
const before = snapshot?.text ?? "";
|
|
31
|
+
const separator = before.length > 0 && !before.endsWith("\n") ? "\n" : "";
|
|
32
|
+
const additions = missing.map((key) => `export ${key}="${DEFAULTS[key]}"`).join("\n");
|
|
33
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
34
|
+
let descriptor = null;
|
|
35
|
+
try {
|
|
36
|
+
descriptor = fs.openSync(
|
|
37
|
+
temporary,
|
|
38
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
39
|
+
snapshot?.mode ?? 384
|
|
40
|
+
);
|
|
41
|
+
fs.writeFileSync(descriptor, `${before}${separator}${additions}
|
|
42
|
+
`);
|
|
43
|
+
if (snapshot) fs.fchmodSync(descriptor, snapshot.mode);
|
|
44
|
+
fs.closeSync(descriptor);
|
|
45
|
+
descriptor = null;
|
|
46
|
+
fs.renameSync(temporary, file);
|
|
47
|
+
} finally {
|
|
48
|
+
if (descriptor !== null) fs.closeSync(descriptor);
|
|
49
|
+
fs.rmSync(temporary, { force: true });
|
|
50
|
+
}
|
|
51
|
+
return missing;
|
|
52
|
+
}
|
|
53
|
+
function readEnvironmentSnapshot(file) {
|
|
54
|
+
let descriptor;
|
|
55
|
+
try {
|
|
56
|
+
descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
const code = error.code;
|
|
59
|
+
if (code === "ENOENT") return null;
|
|
60
|
+
if (code === "ELOOP") {
|
|
61
|
+
throw new Error(`.js.env must not be a symbolic link. Next: replace ${file} with a regular file.`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.fstatSync(descriptor);
|
|
67
|
+
if (!stat.isFile()) {
|
|
68
|
+
throw new Error(`.js.env must be a regular file. Next: replace ${file} with a regular file.`);
|
|
69
|
+
}
|
|
70
|
+
return { text: fs.readFileSync(descriptor, "utf8"), mode: stat.mode & 511 };
|
|
71
|
+
} finally {
|
|
72
|
+
fs.closeSync(descriptor);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function readSetting(source, key) {
|
|
76
|
+
const assignment = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=(.*)$`, "u");
|
|
77
|
+
let value = null;
|
|
78
|
+
for (const line of source.split("\n")) {
|
|
79
|
+
const match = assignment.exec(line);
|
|
80
|
+
if (match) value = normalizeValue(match[1]);
|
|
81
|
+
}
|
|
82
|
+
if (value === null) return { status: "missing", value };
|
|
83
|
+
if (value === "true") return { status: "enabled", value };
|
|
84
|
+
if (value === "false") return { status: "disabled", value };
|
|
85
|
+
return { status: "configured", value };
|
|
86
|
+
}
|
|
87
|
+
function settingForKey(environment, key) {
|
|
88
|
+
return key === "MM_PERPS_ENABLED" ? environment.perpsEnabled : environment.remoteFeatureFlagOverride;
|
|
89
|
+
}
|
|
90
|
+
function normalizeValue(raw) {
|
|
91
|
+
const value = raw.trim();
|
|
92
|
+
if (value.length >= 2) {
|
|
93
|
+
const first = value[0];
|
|
94
|
+
if ((first === '"' || first === "'") && value.at(-1) === first) return value.slice(1, -1);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
ensureMobilePerpsEnvironment,
|
|
100
|
+
mobilePerpsEnvironment
|
|
101
|
+
};
|
|
@@ -6,6 +6,11 @@ import {
|
|
|
6
6
|
decideMobileReadiness,
|
|
7
7
|
launchActions
|
|
8
8
|
} from "./runtime-decision.js";
|
|
9
|
+
import {
|
|
10
|
+
applyMobileMetroEnvPolicy,
|
|
11
|
+
mobileMetroEnvCheck,
|
|
12
|
+
recordMobileMetroEnvBaseline
|
|
13
|
+
} from "./metro-env.js";
|
|
9
14
|
const READY_BRIDGE_CONFIRM_POLLS = 3;
|
|
10
15
|
const POD_PROBE_ENV = {
|
|
11
16
|
FORCE_COLOR: "0",
|
|
@@ -58,11 +63,7 @@ async function prepareMobile(target, opts = {}) {
|
|
|
58
63
|
`
|
|
59
64
|
);
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
const result = await dispatchAction(action, target, platform, json, preflightMode);
|
|
63
|
-
if (result.status !== 0) return result;
|
|
64
|
-
}
|
|
65
|
-
return { status: 0, output: "" };
|
|
66
|
+
return dispatchActionSequence(launchActions(path.resolve(target)), target, platform, json, preflightMode);
|
|
66
67
|
}
|
|
67
68
|
if (report.decision === "unknown") {
|
|
68
69
|
const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
|
|
@@ -73,11 +74,8 @@ async function prepareMobile(target, opts = {}) {
|
|
|
73
74
|
const actions = clearMetro ? report.actions.map(
|
|
74
75
|
(a) => a.id === "start-metro" && !a.argv?.includes("--clear") ? { ...a, argv: [...a.argv ?? [], "--clear"] } : a
|
|
75
76
|
) : report.actions;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (result.status !== 0) return result;
|
|
79
|
-
if (action.id === "yarn-setup") recordDepsBaseline(path.resolve(target));
|
|
80
|
-
}
|
|
77
|
+
const actionResult = await dispatchActionSequence(actions, target, platform, json, preflightMode);
|
|
78
|
+
if (actionResult.status !== 0) return actionResult;
|
|
81
79
|
if (report.decision === "install" && !process.env["RECIPE_UP_INSTALL_ATTEMPTED"]) {
|
|
82
80
|
process.env["RECIPE_UP_INSTALL_ATTEMPTED"] = "1";
|
|
83
81
|
const postInstall = await decideMobileReadiness(target, {
|
|
@@ -100,10 +98,8 @@ async function prepareMobile(target, opts = {}) {
|
|
|
100
98
|
break;
|
|
101
99
|
}
|
|
102
100
|
case "launch": {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (result.status !== 0) return result;
|
|
106
|
-
}
|
|
101
|
+
const result = await dispatchActionSequence(postInstall.actions, target, platform, json, preflightMode);
|
|
102
|
+
if (result.status !== 0) return result;
|
|
107
103
|
break;
|
|
108
104
|
}
|
|
109
105
|
default: {
|
|
@@ -117,6 +113,28 @@ async function prepareMobile(target, opts = {}) {
|
|
|
117
113
|
}
|
|
118
114
|
return { status: 0, output: "" };
|
|
119
115
|
}
|
|
116
|
+
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
|
|
117
|
+
const resolved = path.resolve(target);
|
|
118
|
+
const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
|
|
119
|
+
let pendingMetroEnvFingerprint;
|
|
120
|
+
for (const action of actions) {
|
|
121
|
+
if (action.id === "start-metro") {
|
|
122
|
+
pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
|
|
123
|
+
}
|
|
124
|
+
const result = await dispatchAction(action, target, platform, json, preflightMode);
|
|
125
|
+
if (result.status !== 0) return result;
|
|
126
|
+
if (action.id === "yarn-setup") recordDepsBaseline(resolved);
|
|
127
|
+
if (action.id === "prewarm-bundle" && pendingMetroEnvFingerprint) {
|
|
128
|
+
if (!recordMobileMetroEnvBaseline(resolved, pendingMetroEnvFingerprint)) {
|
|
129
|
+
const message = "Mobile environment changed while Metro was starting; the stale bundle baseline was not recorded.\n Next: re-run mm-harness launch.";
|
|
130
|
+
if (!json) process.stderr.write(`${message}
|
|
131
|
+
`);
|
|
132
|
+
return { status: EXIT.runtime, output: message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { status: 0, output: "" };
|
|
137
|
+
}
|
|
120
138
|
async function dispatchAction(action, target, platform, json, preflightMode = "fast") {
|
|
121
139
|
const cwd = action.cwd ?? target;
|
|
122
140
|
switch (action.id) {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
|
|
15
15
|
import { recipeRuntimePath } from "../../paths.js";
|
|
16
16
|
import { mobileProductMarkers } from "./deps-markers.js";
|
|
17
|
+
import { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
|
|
17
18
|
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
18
19
|
const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u;
|
|
19
20
|
const NATIVE_MODULE_STALE = /\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
|
|
@@ -113,11 +114,29 @@ async function decideMobileReadiness(target, options = {}) {
|
|
|
113
114
|
"Fast preflight found dependencies not ready and does not install them (the orchestrator deps phase owns installation).",
|
|
114
115
|
...report.reasons
|
|
115
116
|
],
|
|
116
|
-
userAction: `
|
|
117
|
+
userAction: `1. Standalone checkout \u2014 run ${standalone} in the checkout.
|
|
118
|
+
2. Orchestrated slot \u2014 run its deps/prepare phase.
|
|
119
|
+
After the selected action succeeds, re-run \`mm-harness launch <platform>\`.`,
|
|
117
120
|
actions: []
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
|
-
|
|
123
|
+
const env = mobileMetroEnvCheck(resolved);
|
|
124
|
+
if (env.status === "current" || report.decision !== "launch" && report.decision !== "ready") {
|
|
125
|
+
return report;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
...report,
|
|
129
|
+
decision: "launch",
|
|
130
|
+
reasonCode: report.decision === "ready" ? env.status === "missing" ? "metro-env-baseline-missing" : "metro-env-changed" : report.reasonCode,
|
|
131
|
+
reasons: [
|
|
132
|
+
env.status === "missing" ? "Metro environment has no successful bundle baseline; restart once with a cleared transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart once with a cleared transform cache.",
|
|
133
|
+
...report.reasons
|
|
134
|
+
],
|
|
135
|
+
actions: applyMobileMetroEnvPolicy(
|
|
136
|
+
resolved,
|
|
137
|
+
report.decision === "ready" ? launchActions(resolved) : report.actions
|
|
138
|
+
)
|
|
139
|
+
};
|
|
121
140
|
}
|
|
122
141
|
async function computeMobileReadiness(resolved, options, fast) {
|
|
123
142
|
const rawDeps = depsCheck(resolved, {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { recipeRuntimePath } from "../../paths.js";
|
|
6
|
+
const SOURCE_PATHS = [
|
|
7
|
+
"app",
|
|
8
|
+
"locales",
|
|
9
|
+
"index.js",
|
|
10
|
+
"shim.js",
|
|
11
|
+
"shimPerf.js",
|
|
12
|
+
"ReactotronConfig.js",
|
|
13
|
+
"wdyr.js",
|
|
14
|
+
"app.config.js",
|
|
15
|
+
"babel.config.js",
|
|
16
|
+
"metro.config.js",
|
|
17
|
+
"metro.transform.js",
|
|
18
|
+
"package.json"
|
|
19
|
+
];
|
|
20
|
+
const ENV_INPUTS = [".js.env", ".env", ".env.local"];
|
|
21
|
+
const BASELINE_FILE = "mobile-source-baseline.json";
|
|
22
|
+
function mobileSourceFingerprint(target) {
|
|
23
|
+
try {
|
|
24
|
+
const hash = createHash("sha256");
|
|
25
|
+
hash.update(git(target, ["rev-parse", "HEAD"]));
|
|
26
|
+
hash.update(git(target, ["diff", "--no-ext-diff", "--binary", "HEAD", "--", ...SOURCE_PATHS]));
|
|
27
|
+
const untracked = git(target, [
|
|
28
|
+
"ls-files",
|
|
29
|
+
"--others",
|
|
30
|
+
"--exclude-standard",
|
|
31
|
+
"-z",
|
|
32
|
+
"--",
|
|
33
|
+
...SOURCE_PATHS
|
|
34
|
+
]).toString("utf8").split("\0").filter(Boolean).sort();
|
|
35
|
+
for (const relative of untracked) {
|
|
36
|
+
hash.update(`untracked\0${relative}\0`);
|
|
37
|
+
hash.update(fs.readFileSync(path.join(target, relative)));
|
|
38
|
+
hash.update("\0");
|
|
39
|
+
}
|
|
40
|
+
for (const relative of ENV_INPUTS) {
|
|
41
|
+
const absolute = path.join(target, relative);
|
|
42
|
+
hash.update(`env\0${relative}\0`);
|
|
43
|
+
hash.update(fs.existsSync(absolute) ? fs.readFileSync(absolute) : "<absent>");
|
|
44
|
+
hash.update("\0");
|
|
45
|
+
}
|
|
46
|
+
return { status: "ok", fingerprint: hash.digest("hex") };
|
|
47
|
+
} catch {
|
|
48
|
+
const resolved = path.resolve(target);
|
|
49
|
+
return {
|
|
50
|
+
fingerprint: null,
|
|
51
|
+
status: "unavailable",
|
|
52
|
+
error: {
|
|
53
|
+
code: "MOBILE_SOURCE_UNAVAILABLE",
|
|
54
|
+
message: `Mobile source fingerprint requires a readable Git checkout and source inputs: ${resolved}`,
|
|
55
|
+
userAction: "mm-harness doctor --adapter mobile --target <metamask-mobile-checkout>"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function mobileSourceCheck(target) {
|
|
61
|
+
const result = mobileSourceFingerprint(target);
|
|
62
|
+
if (result.status === "unavailable") return result;
|
|
63
|
+
const { fingerprint } = result;
|
|
64
|
+
const baseline = readBaseline(target);
|
|
65
|
+
return {
|
|
66
|
+
fingerprint,
|
|
67
|
+
status: baseline === null ? "missing" : baseline.fingerprint === fingerprint ? "current" : "changed"
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function recordMobileSourceBaseline(target, expectedFingerprint) {
|
|
71
|
+
if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) return false;
|
|
72
|
+
const marker = recipeRuntimePath(target, BASELINE_FILE);
|
|
73
|
+
const temporary = `${marker}.${process.pid}.${Date.now()}.tmp`;
|
|
74
|
+
const baseline = {
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
fingerprint: expectedFingerprint,
|
|
77
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
78
|
+
};
|
|
79
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
80
|
+
try {
|
|
81
|
+
fs.writeFileSync(temporary, `${JSON.stringify(baseline, null, 2)}
|
|
82
|
+
`, { mode: 384 });
|
|
83
|
+
if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) return false;
|
|
84
|
+
fs.renameSync(temporary, marker);
|
|
85
|
+
if (mobileSourceFingerprint(target).fingerprint !== expectedFingerprint) {
|
|
86
|
+
if (readBaseline(target)?.fingerprint === expectedFingerprint) fs.rmSync(marker, { force: true });
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(temporary, { force: true });
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function git(target, args) {
|
|
95
|
+
return execFileSync("git", args, {
|
|
96
|
+
cwd: target,
|
|
97
|
+
encoding: "buffer",
|
|
98
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
99
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function readBaseline(target) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(
|
|
105
|
+
fs.readFileSync(recipeRuntimePath(target, BASELINE_FILE), "utf8")
|
|
106
|
+
);
|
|
107
|
+
return parsed.schemaVersion === 1 && typeof parsed.fingerprint === "string" ? parsed : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
mobileSourceCheck,
|
|
114
|
+
mobileSourceFingerprint,
|
|
115
|
+
recordMobileSourceBaseline
|
|
116
|
+
};
|
|
@@ -46,6 +46,9 @@ const mobileSurface = {
|
|
|
46
46
|
logSources(target) {
|
|
47
47
|
return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
|
|
48
48
|
},
|
|
49
|
+
appLogSource(target) {
|
|
50
|
+
return { label: "app", path: recipeRuntimePath(target, "metro.log") };
|
|
51
|
+
},
|
|
49
52
|
hints: {
|
|
50
53
|
launch: "mm-harness launch ios",
|
|
51
54
|
relaunch: "mm-harness launch ios"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
formatKvLines,
|
|
3
|
+
formatExtensionSlotPorts,
|
|
3
4
|
inferSlotSuffix,
|
|
4
5
|
realRepoPath,
|
|
5
6
|
resolveDefaultExtensionPorts,
|
|
7
|
+
resolveExtensionRuntimeContext,
|
|
6
8
|
resolveExtensionRuntimePorts,
|
|
7
9
|
resolveSlotPortsByRepo,
|
|
8
10
|
resolveFarmslotPortsByRepo,
|
|
@@ -11,10 +13,12 @@ import {
|
|
|
11
13
|
resolveMobileSlotDefaults
|
|
12
14
|
} from "../../adapters/shared/resolve-slot-ports-core.mjs";
|
|
13
15
|
export {
|
|
16
|
+
formatExtensionSlotPorts,
|
|
14
17
|
formatKvLines,
|
|
15
18
|
inferSlotSuffix,
|
|
16
19
|
realRepoPath,
|
|
17
20
|
resolveDefaultExtensionPorts,
|
|
21
|
+
resolveExtensionRuntimeContext,
|
|
18
22
|
resolveExtensionRuntimePorts,
|
|
19
23
|
resolveFarmslotPortsByRepo,
|
|
20
24
|
resolveMobileRuntimeContext,
|