@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
|
@@ -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
|
+
};
|
|
@@ -12,7 +12,8 @@ const mobileSurface = {
|
|
|
12
12
|
},
|
|
13
13
|
async runtimeStatus(target) {
|
|
14
14
|
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
15
|
-
const
|
|
15
|
+
const platform = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
16
|
+
const report = await mobileRuntimeStatus(target, { watcherPort, platform });
|
|
16
17
|
const runwayProvisioned = hasRunwayProvisionBaseline(target);
|
|
17
18
|
const depsPending = runwayProvisioned && report.checks?.deps?.status !== "current";
|
|
18
19
|
return {
|
|
@@ -45,6 +46,9 @@ const mobileSurface = {
|
|
|
45
46
|
logSources(target) {
|
|
46
47
|
return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
|
|
47
48
|
},
|
|
49
|
+
appLogSource(target) {
|
|
50
|
+
return { label: "app", path: recipeRuntimePath(target, "metro.log") };
|
|
51
|
+
},
|
|
48
52
|
hints: {
|
|
49
53
|
launch: "mm-harness launch ios",
|
|
50
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,
|