@deeeed/metamask-harness 0.3.9 → 0.5.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 +45 -1
- package/dist/adapters/core/surface.js +53 -0
- package/dist/adapters/extension/ensure-ready.js +109 -0
- package/dist/adapters/extension/extension-id.js +62 -0
- package/dist/adapters/extension/runtime-decision.js +305 -0
- package/dist/adapters/extension/runtime.js +324 -0
- package/dist/adapters/extension/surface.js +69 -0
- package/dist/adapters/mobile/deps-markers.js +22 -0
- package/dist/adapters/mobile/prepare.js +146 -0
- package/dist/adapters/mobile/provision.js +465 -0
- package/dist/adapters/mobile/runtime-decision.js +315 -0
- package/dist/adapters/mobile/surface.js +54 -0
- package/dist/adapters/slot-ports.js +146 -0
- package/dist/adapters/surface.js +14 -0
- package/dist/adapters.js +485 -0
- package/dist/cli-color.js +79 -0
- package/dist/cli-commands.js +224 -0
- package/dist/cli-version.js +111 -0
- package/dist/cli.js +1571 -0
- package/dist/commands/debug.js +56 -0
- package/dist/commands/fixtures.js +153 -0
- package/dist/commands/launch.js +325 -0
- package/dist/commands/logs.js +73 -0
- package/dist/commands/shared.js +157 -0
- package/dist/commands/update.js +243 -0
- package/dist/completions-cache.js +53 -0
- package/dist/doctor.js +169 -0
- package/dist/harness.js +627 -0
- package/dist/heal-bounds.js +120 -0
- package/dist/index.js +25 -0
- package/dist/leaf-invoke.js +19 -0
- package/dist/live-adapter-contract.js +240 -0
- package/dist/manifest.js +37 -0
- package/dist/mm-harness-cli.js +521 -0
- package/dist/paths.js +179 -0
- package/dist/progress.js +94 -0
- package/dist/recording-target.js +133 -0
- package/dist/run-recording.js +271 -0
- package/dist/runner.js +88 -0
- package/dist/types.js +0 -0
- package/docs/ADAPTER-SURFACE.md +119 -0
- package/docs/CLI-SPEC.md +26 -3
- package/docs/UX-PRINCIPLES.md +3 -0
- package/package.json +10 -2
- package/src/adapters/core/surface.ts +71 -0
- package/src/adapters/extension/surface.ts +88 -0
- package/src/adapters/mobile/provision.ts +594 -0
- package/src/adapters/mobile/surface.ts +71 -0
- package/src/adapters/slot-ports.ts +165 -0
- package/src/adapters/surface.ts +117 -0
- package/src/cli-commands.ts +1 -1
- package/src/cli.ts +239 -49
- package/src/commands/debug.ts +3 -1
- package/src/commands/fixtures.ts +13 -8
- package/src/commands/launch.ts +7 -156
- package/src/commands/logs.ts +29 -13
- package/src/harness.ts +140 -3
- package/src/mm-harness-cli.ts +71 -18
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runnerDir } from "../paths.js";
|
|
3
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
|
|
5
|
+
const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json"]);
|
|
6
|
+
async function handleDebug(argv) {
|
|
7
|
+
const { options } = parseFlags(argv, DEBUG_BOOLEANS);
|
|
8
|
+
const json = flag(options, "json");
|
|
9
|
+
const target = targetOf(options);
|
|
10
|
+
const adapter = resolveAdapter(options, target);
|
|
11
|
+
if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
12
|
+
const surface = getAdapterSurface(adapter);
|
|
13
|
+
if (surface.headless) return usageOut(json, "debug", "core is headless; there is no debug console.", surface.hints.relaunch);
|
|
14
|
+
const worker = flag(options, "worker");
|
|
15
|
+
const devMenu = flag(options, "devMenu");
|
|
16
|
+
if (adapter === "mobile" && worker) {
|
|
17
|
+
return usageOut(
|
|
18
|
+
json,
|
|
19
|
+
"debug",
|
|
20
|
+
"--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.",
|
|
21
|
+
"mm-harness debug --dev-menu"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (adapter === "extension" && devMenu) {
|
|
25
|
+
return usageOut(
|
|
26
|
+
json,
|
|
27
|
+
"debug",
|
|
28
|
+
"--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.",
|
|
29
|
+
"mm-harness debug --worker"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
|
|
33
|
+
const debugArgs = ["--adapter", adapter];
|
|
34
|
+
if (adapter === "extension") {
|
|
35
|
+
if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
36
|
+
debugArgs.push("--target", worker ? "worker" : "page");
|
|
37
|
+
} else {
|
|
38
|
+
if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
|
|
39
|
+
debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
|
|
40
|
+
}
|
|
41
|
+
if (json) debugArgs.push("--json");
|
|
42
|
+
const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
|
|
43
|
+
if (json) {
|
|
44
|
+
console.log(
|
|
45
|
+
JSON.stringify(
|
|
46
|
+
{ schemaVersion: 1, command: "debug", adapter, mode: worker ? "worker" : devMenu ? "dev-menu" : "default", exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
47
|
+
null,
|
|
48
|
+
2
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
handleDebug
|
|
56
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runnerDir, walletFixturePath } from "../paths.js";
|
|
4
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from "./shared.js";
|
|
6
|
+
const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
7
|
+
const RECOVERABLE_SETUP_WALLET_PATTERNS = [
|
|
8
|
+
"CDP not reachable",
|
|
9
|
+
"CDP bridge",
|
|
10
|
+
"is not installed",
|
|
11
|
+
"Engine.context.KeyringController not available",
|
|
12
|
+
"Engine does not exist",
|
|
13
|
+
"Cannot read property 'transactions'",
|
|
14
|
+
"bridge-not-ready",
|
|
15
|
+
"debug-target-missing",
|
|
16
|
+
"CDP eval failed",
|
|
17
|
+
"CDP eval-async failed"
|
|
18
|
+
];
|
|
19
|
+
function isRecoverableSetupWalletFailure(output) {
|
|
20
|
+
return RECOVERABLE_SETUP_WALLET_PATTERNS.some((p) => output.includes(p));
|
|
21
|
+
}
|
|
22
|
+
async function handleFixtures(argv, deps) {
|
|
23
|
+
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
24
|
+
const json = flag(options, "json");
|
|
25
|
+
const sub = positional[0];
|
|
26
|
+
if (sub !== "sync" && sub !== "set") {
|
|
27
|
+
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <sync|set>", "mm-harness fixtures sync or mm-harness fixtures set");
|
|
28
|
+
}
|
|
29
|
+
const target = targetOf(options);
|
|
30
|
+
const adapter = resolveAdapter(options, target);
|
|
31
|
+
if (!adapter) {
|
|
32
|
+
return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
33
|
+
}
|
|
34
|
+
const surface = getAdapterSurface(adapter);
|
|
35
|
+
if (surface.headless) return usageOut(json, "fixtures", "core is headless; it has no wallet fixture.", surface.hints.launch);
|
|
36
|
+
const canonicalFixture = walletFixturePath(target);
|
|
37
|
+
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures set`;
|
|
38
|
+
if (sub === "sync") {
|
|
39
|
+
const exitCode = fixturesSync(adapter, target, json);
|
|
40
|
+
if (json) {
|
|
41
|
+
console.log(
|
|
42
|
+
JSON.stringify(
|
|
43
|
+
{ schemaVersion: 1, command: "fixtures", action: "sync", adapter, fixture: canonicalFixture, exitCode },
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return exitCode;
|
|
50
|
+
}
|
|
51
|
+
const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
52
|
+
let status;
|
|
53
|
+
if (adapter === "mobile") {
|
|
54
|
+
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
55
|
+
const previousAppRoot = process.env.APP_ROOT;
|
|
56
|
+
process.env.APP_ROOT = target;
|
|
57
|
+
try {
|
|
58
|
+
let result = spawnScript(setupWalletSh, ["--fixture", fixturePath], target, json);
|
|
59
|
+
if (result.status !== 0 && isRecoverableSetupWalletFailure(result.output) && !process.env["RECIPE_SETUP_WALLET_RETRIED"]) {
|
|
60
|
+
process.env["RECIPE_SETUP_WALLET_RETRIED"] = "1";
|
|
61
|
+
if (!json) process.stderr.write(" setup-wallet: recoverable failure \u2014 restarting Metro and retrying\n Next: wait for relaunch, then wallet setup will retry automatically\n");
|
|
62
|
+
const { prepareMobile } = await import("../adapters/mobile/prepare.js");
|
|
63
|
+
const platform = str(options, "platform") ?? process.env["MOBILE_PLATFORM"] ?? "ios";
|
|
64
|
+
const relaunchResult = await prepareMobile(target, { platform, json, preflightMode: "auto", clearMetro: true });
|
|
65
|
+
if (relaunchResult.status === 0) {
|
|
66
|
+
result = spawnScript(setupWalletSh, ["--fixture", fixturePath], target, json);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
status = result.status === 0 ? "pass" : "fail";
|
|
70
|
+
} finally {
|
|
71
|
+
delete process.env["RECIPE_SETUP_WALLET_RETRIED"];
|
|
72
|
+
if (previousAppRoot === void 0) delete process.env.APP_ROOT;
|
|
73
|
+
else process.env.APP_ROOT = previousAppRoot;
|
|
74
|
+
}
|
|
75
|
+
if (!json && status === "fail") {
|
|
76
|
+
console.error(` Next: ${retryHint}`);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
if (!fs.existsSync(fixturePath)) {
|
|
80
|
+
return usageOut(
|
|
81
|
+
json,
|
|
82
|
+
"fixtures",
|
|
83
|
+
`no wallet fixture at ${fixturePath}.`,
|
|
84
|
+
"create it (or pass --fixture <path>), then re-run: mm-harness fixtures set"
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
88
|
+
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
89
|
+
try {
|
|
90
|
+
const result = await deps.runOneNode("extension", "metamask.wallet.setup", {}, target, str(options, "actionManifest"));
|
|
91
|
+
status = result.status;
|
|
92
|
+
} finally {
|
|
93
|
+
if (previousFixtureEnv === void 0) delete process.env.RECIPE_WALLET_FIXTURE;
|
|
94
|
+
else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
|
|
95
|
+
}
|
|
96
|
+
if (!json && status === "fail") {
|
|
97
|
+
console.error(` Next: ${retryHint}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
|
|
101
|
+
if (json) {
|
|
102
|
+
console.log(
|
|
103
|
+
JSON.stringify(
|
|
104
|
+
{
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
command: "fixtures",
|
|
107
|
+
action: "set",
|
|
108
|
+
adapter,
|
|
109
|
+
fixture: fixturePath,
|
|
110
|
+
canonicalFixture,
|
|
111
|
+
status,
|
|
112
|
+
exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
113
|
+
message: teaching,
|
|
114
|
+
error: status === "fail" ? {
|
|
115
|
+
code: "SETUP_WALLET_FAILED",
|
|
116
|
+
message: "wallet fixture setup failed",
|
|
117
|
+
userAction: retryHint
|
|
118
|
+
} : null
|
|
119
|
+
},
|
|
120
|
+
null,
|
|
121
|
+
2
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
console.error(teaching);
|
|
126
|
+
}
|
|
127
|
+
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
128
|
+
}
|
|
129
|
+
function fixturesSync(adapter, target, json) {
|
|
130
|
+
const mmHarnessBin = path.join(runnerDir, "bin/mm-harness");
|
|
131
|
+
const installResult = spawnScript(
|
|
132
|
+
mmHarnessBin,
|
|
133
|
+
["install", "--adapter", adapter, "--target", target],
|
|
134
|
+
target,
|
|
135
|
+
json
|
|
136
|
+
);
|
|
137
|
+
if (installResult.status !== 0) {
|
|
138
|
+
if (!json) {
|
|
139
|
+
console.error(`\u2717 mm-harness fixtures sync: overlay reinstall failed.
|
|
140
|
+
Next: mm-harness install --target ${target} # diagnose the install failure`);
|
|
141
|
+
}
|
|
142
|
+
return EXIT.runtime;
|
|
143
|
+
}
|
|
144
|
+
const syncFixtureSh = path.join(runnerDir, "adapters/shared/sync-wallet-fixture.sh");
|
|
145
|
+
const syncArgs = ["--target", target];
|
|
146
|
+
if (process.env.CDP_PORT) syncArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
147
|
+
if (process.env.RECIPE_SLOT_ID) syncArgs.push("--slot-id", process.env.RECIPE_SLOT_ID);
|
|
148
|
+
spawnScript(syncFixtureSh, syncArgs, target, json);
|
|
149
|
+
return EXIT.ok;
|
|
150
|
+
}
|
|
151
|
+
export {
|
|
152
|
+
handleFixtures
|
|
153
|
+
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { color } from "../cli-color.js";
|
|
5
|
+
import { handleHarness } from "../harness.js";
|
|
6
|
+
import { recipeHarnessPath, recipeRuntimeDir, recipeRuntimePath, runnerDir } from "../paths.js";
|
|
7
|
+
import { prepareMobile } from "../adapters/mobile/prepare.js";
|
|
8
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
9
|
+
import { stopExtensionWatcher } from "../adapters/slot-ports.js";
|
|
10
|
+
import {
|
|
11
|
+
ADAPTER_DETECT_NEXT,
|
|
12
|
+
EXIT,
|
|
13
|
+
flag,
|
|
14
|
+
parseFlags,
|
|
15
|
+
resolveAdapter,
|
|
16
|
+
spawnScript,
|
|
17
|
+
spawnScriptStreaming,
|
|
18
|
+
str,
|
|
19
|
+
targetOf,
|
|
20
|
+
usageOut
|
|
21
|
+
} from "./shared.js";
|
|
22
|
+
import {
|
|
23
|
+
RECOVERY_CODE,
|
|
24
|
+
checkHealBounds,
|
|
25
|
+
ensureOverlay,
|
|
26
|
+
newHealState,
|
|
27
|
+
parseHeal
|
|
28
|
+
} from "../heal-bounds.js";
|
|
29
|
+
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
30
|
+
"build",
|
|
31
|
+
"watch",
|
|
32
|
+
"verify",
|
|
33
|
+
"sidepanel",
|
|
34
|
+
"fullscreen",
|
|
35
|
+
"runway",
|
|
36
|
+
"json",
|
|
37
|
+
"jsonStream",
|
|
38
|
+
"yes"
|
|
39
|
+
]);
|
|
40
|
+
async function handleLaunch(argv) {
|
|
41
|
+
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
42
|
+
const json = flag(options, "json");
|
|
43
|
+
const target = targetOf(options);
|
|
44
|
+
const heal = parseHeal(options, "auto");
|
|
45
|
+
if (typeof heal !== "string") return usageOut(json, "launch", heal.error, "use --heal off|infra-only|auto");
|
|
46
|
+
const posToken = positional[0];
|
|
47
|
+
const mobileTargetToken = posToken === "ios" || posToken === "android" ? posToken : void 0;
|
|
48
|
+
const platformFlag = str(options, "platform");
|
|
49
|
+
const platformMobileToken = platformFlag === "ios" || platformFlag === "android" ? platformFlag : void 0;
|
|
50
|
+
const mobileTarget = mobileTargetToken ?? platformMobileToken;
|
|
51
|
+
const adapterHint = mobileTarget ? "mobile" : void 0;
|
|
52
|
+
const adapter = resolveAdapter(options, target, adapterHint);
|
|
53
|
+
if (!adapter) {
|
|
54
|
+
return usageOut(json, "launch", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
55
|
+
}
|
|
56
|
+
if (adapter === "core") {
|
|
57
|
+
return usageOut(json, "launch", "core is headless; there is nothing to launch.", "mm-harness verify");
|
|
58
|
+
}
|
|
59
|
+
if (adapter === "mobile" && !mobileTarget) {
|
|
60
|
+
return usageOut(
|
|
61
|
+
json,
|
|
62
|
+
"launch",
|
|
63
|
+
"target is required for mobile.",
|
|
64
|
+
"mm-harness launch ios or mm-harness launch android"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const wantBuild = flag(options, "build");
|
|
68
|
+
const wantVerify = flag(options, "verify");
|
|
69
|
+
const wantWatch = flag(options, "watch");
|
|
70
|
+
const wantRunway = flag(options, "runway");
|
|
71
|
+
if (wantRunway && adapter !== "mobile") {
|
|
72
|
+
return usageOut(json, "launch", "runway is mobile-only.", "drop --runway for the extension");
|
|
73
|
+
}
|
|
74
|
+
const displayMode = flag(options, "sidepanel") && !flag(options, "fullscreen") ? "sidepanel" : "fullscreen";
|
|
75
|
+
for (const portFlag of ["cdpPort", "watcherPort"]) {
|
|
76
|
+
const value = str(options, portFlag);
|
|
77
|
+
if (value !== void 0 && !/^\d+$/u.test(value)) {
|
|
78
|
+
return usageOut(
|
|
79
|
+
json,
|
|
80
|
+
"launch",
|
|
81
|
+
`--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
|
|
82
|
+
`pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
87
|
+
const tier = wantVerify ? "verify" : wantBuild ? "build" : "quick";
|
|
88
|
+
const state = newHealState();
|
|
89
|
+
if (tier === "quick" && nativeInputsChanged(target, adapter)) {
|
|
90
|
+
return usageOut(
|
|
91
|
+
json,
|
|
92
|
+
"launch",
|
|
93
|
+
"native build inputs changed since last build.",
|
|
94
|
+
`mm-harness launch ${adapter === "mobile" ? `${mobileTarget} ` : ""}--build`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
98
|
+
if (!ensured.ok) {
|
|
99
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
100
|
+
code: "OVERLAY_INSTALL_FAILED",
|
|
101
|
+
message: ensured.error ?? "overlay install failed",
|
|
102
|
+
recoverable: false,
|
|
103
|
+
exitCode: EXIT.infra
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
if (tier === "verify") {
|
|
107
|
+
const liveArgs = ["live", "--adapter", adapter, "--target", target];
|
|
108
|
+
if (adapter === "mobile" && mobileTarget) liveArgs.push("--platform", mobileTarget);
|
|
109
|
+
if (json) liveArgs.push("--json");
|
|
110
|
+
const code = await handleHarness(liveArgs);
|
|
111
|
+
return code;
|
|
112
|
+
}
|
|
113
|
+
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
114
|
+
if (attempt.status === 0) {
|
|
115
|
+
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
116
|
+
}
|
|
117
|
+
if (heal === "off") {
|
|
118
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
119
|
+
code: "LAUNCH_FAILED",
|
|
120
|
+
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed (healing off)`,
|
|
121
|
+
recoverable: false,
|
|
122
|
+
exitCode: EXIT.infra
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const bound = checkHealBounds(target, attempt.output, state);
|
|
126
|
+
if (bound !== null) {
|
|
127
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
128
|
+
code: bound.code,
|
|
129
|
+
message: bound.message,
|
|
130
|
+
recoverable: false,
|
|
131
|
+
userAction: bound.userAction,
|
|
132
|
+
exitCode: bound.exitCode,
|
|
133
|
+
originalError: bound.originalError
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const recoveryCode = RECOVERY_CODE[adapter];
|
|
137
|
+
state.attemptedRecoveries.push(recoveryCode);
|
|
138
|
+
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json);
|
|
139
|
+
if (attempt.status === 0) {
|
|
140
|
+
state.recovered.push(recoveryCode);
|
|
141
|
+
return finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state);
|
|
142
|
+
}
|
|
143
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
144
|
+
code: "SAME_RECOVERY_TWICE",
|
|
145
|
+
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
|
|
146
|
+
recoverable: false,
|
|
147
|
+
exitCode: EXIT.bounded,
|
|
148
|
+
originalError: attempt.output.trim() || void 0
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function applyLaunchEnvOverrides(options, adapter, mobileTarget, target) {
|
|
152
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
153
|
+
const device = str(options, "device");
|
|
154
|
+
if (device && adapter === "mobile") {
|
|
155
|
+
if (mobileTarget === "android") {
|
|
156
|
+
process.env.ADB_SERIAL = device;
|
|
157
|
+
process.env.ANDROID_SERIAL = device;
|
|
158
|
+
process.env.ANDROID_DEVICE = device;
|
|
159
|
+
} else {
|
|
160
|
+
process.env.IOS_SIMULATOR = device;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const cdpPort = str(options, "cdpPort");
|
|
164
|
+
if (cdpPort) {
|
|
165
|
+
process.env.CDP_PORT = cdpPort;
|
|
166
|
+
process.env.RECIPE_CDP_PORT = cdpPort;
|
|
167
|
+
}
|
|
168
|
+
const watcherPort = str(options, "watcherPort");
|
|
169
|
+
if (watcherPort) {
|
|
170
|
+
process.env.WATCHER_PORT = watcherPort;
|
|
171
|
+
process.env.RECIPE_WATCHER_PORT = watcherPort;
|
|
172
|
+
process.env.METRO_PORT = watcherPort;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function nativeInputsChanged(target, adapter) {
|
|
176
|
+
const baselineFile = recipeRuntimePath(target, ".last-build-ref");
|
|
177
|
+
let baseline;
|
|
178
|
+
try {
|
|
179
|
+
baseline = fs.readFileSync(baselineFile, "utf8").trim();
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (!baseline) return false;
|
|
184
|
+
const paths = adapter === "mobile" ? ["ios", "android", "package.json", "yarn.lock"] : ["package.json", "yarn.lock", "webpack.config.js"];
|
|
185
|
+
try {
|
|
186
|
+
const diff = execFileSync("git", ["-C", target, "diff", "--name-only", baseline, "--", ...paths], {
|
|
187
|
+
encoding: "utf8",
|
|
188
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
189
|
+
}).trim();
|
|
190
|
+
return diff.length > 0;
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json) {
|
|
196
|
+
if (adapter === "mobile") {
|
|
197
|
+
const platform = mobileTarget ?? "ios";
|
|
198
|
+
const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
|
|
199
|
+
const preflightMode = tier === "build" ? "auto" : "fast";
|
|
200
|
+
return prepareMobile(target, { platform, json, watcherPort, preflightMode });
|
|
201
|
+
}
|
|
202
|
+
if (!wantWatch && tier === "quick") {
|
|
203
|
+
return extensionRebuild(target, json);
|
|
204
|
+
}
|
|
205
|
+
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
206
|
+
const watchArgs = ["--target", target];
|
|
207
|
+
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
208
|
+
console.error(`\u2192 extension ${wantWatch ? "watch" : "build"} \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
209
|
+
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
210
|
+
}
|
|
211
|
+
async function extensionRebuild(target, json) {
|
|
212
|
+
const runtimeDirRel = recipeRuntimeDir();
|
|
213
|
+
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
214
|
+
const rebuildLog = path.join(runtimeAbs, "rebuild.log");
|
|
215
|
+
stopExtensionWatcher(target);
|
|
216
|
+
fs.mkdirSync(path.dirname(rebuildLog), { recursive: true });
|
|
217
|
+
fs.writeFileSync(rebuildLog, "");
|
|
218
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
219
|
+
const liveArgs = ["--target", target, "--start-watch"];
|
|
220
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
221
|
+
if (process.env.WATCHER_PORT) liveArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
222
|
+
console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
|
|
223
|
+
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
224
|
+
if (result.output) {
|
|
225
|
+
try {
|
|
226
|
+
fs.appendFileSync(rebuildLog, result.output);
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
function finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state) {
|
|
233
|
+
if (adapter === "extension" && displayMode === "sidepanel") {
|
|
234
|
+
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
235
|
+
const sidepanelArgs = ["open"];
|
|
236
|
+
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
237
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json);
|
|
238
|
+
if (sidepanel.status !== 0) {
|
|
239
|
+
return launchFail(json, adapter, mobileTarget, tier, state, {
|
|
240
|
+
code: "SIDEPANEL_OPEN_FAILED",
|
|
241
|
+
message: "app launched but opening the side panel failed.",
|
|
242
|
+
recoverable: false,
|
|
243
|
+
userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
|
|
244
|
+
exitCode: EXIT.runtime,
|
|
245
|
+
originalError: sidepanel.output.trim() || void 0
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return launchPass(json, adapter, mobileTarget, tier, displayMode, state);
|
|
250
|
+
}
|
|
251
|
+
function launchPass(json, adapter, mobileTarget, tier, displayMode, state) {
|
|
252
|
+
if (json) {
|
|
253
|
+
console.log(
|
|
254
|
+
JSON.stringify(
|
|
255
|
+
{
|
|
256
|
+
schemaVersion: 1,
|
|
257
|
+
command: "launch",
|
|
258
|
+
adapter,
|
|
259
|
+
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
260
|
+
tier,
|
|
261
|
+
status: "pass",
|
|
262
|
+
phase: "launch",
|
|
263
|
+
recovered: state.recovered,
|
|
264
|
+
mutations: state.mutations,
|
|
265
|
+
exitCode: EXIT.ok
|
|
266
|
+
},
|
|
267
|
+
null,
|
|
268
|
+
2
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
} else {
|
|
272
|
+
const device = adapter === "mobile" ? process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device" : displayMode;
|
|
273
|
+
const tierNote = tier === "quick" ? "quick relaunch, no native build" : tier;
|
|
274
|
+
const devNote = process.env.MM_HARNESS_BIN ? ` ${color("dim", "[dev: MM_HARNESS_BIN]")}` : "";
|
|
275
|
+
console.error(
|
|
276
|
+
`${color("ok", "\u2713")} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} \u2014 ${color("ok", String(device))} \xB7 ${tierNote} \xB7 app + bridge ready${devNote}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return EXIT.ok;
|
|
280
|
+
}
|
|
281
|
+
function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
282
|
+
if (json) {
|
|
283
|
+
console.log(
|
|
284
|
+
JSON.stringify(
|
|
285
|
+
{
|
|
286
|
+
schemaVersion: 1,
|
|
287
|
+
command: "launch",
|
|
288
|
+
adapter,
|
|
289
|
+
platform: mobileTarget ?? null,
|
|
290
|
+
tier,
|
|
291
|
+
status: "fail",
|
|
292
|
+
phase: state.attemptedRecoveries.length ? "recover" : "launch",
|
|
293
|
+
recovered: state.recovered,
|
|
294
|
+
mutations: state.mutations,
|
|
295
|
+
recoverable: failure.recoverable,
|
|
296
|
+
attemptedRecoveries: state.attemptedRecoveries,
|
|
297
|
+
exitCode: failure.exitCode,
|
|
298
|
+
error: {
|
|
299
|
+
code: failure.code,
|
|
300
|
+
message: failure.message,
|
|
301
|
+
retryable: failure.recoverable,
|
|
302
|
+
userAction: failure.userAction ?? null,
|
|
303
|
+
// The classification note is in `message`; the REAL failure text lives
|
|
304
|
+
// here verbatim so it is never lost.
|
|
305
|
+
originalError: failure.originalError ?? null
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
null,
|
|
309
|
+
2
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
} else {
|
|
313
|
+
console.error(
|
|
314
|
+
`\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
|
|
315
|
+
${failure.message}` + (failure.originalError ? `
|
|
316
|
+
--- original failure ---
|
|
317
|
+
${failure.originalError}` : "") + (failure.userAction ? `
|
|
318
|
+
Next: ${failure.userAction}` : "")
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return failure.exitCode;
|
|
322
|
+
}
|
|
323
|
+
export {
|
|
324
|
+
handleLaunch
|
|
325
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runnerDir } from "../paths.js";
|
|
4
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
5
|
+
import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, str, targetOf, usageOut } from "./shared.js";
|
|
6
|
+
const LOGS_BOOLEANS = /* @__PURE__ */ new Set(["full", "json"]);
|
|
7
|
+
async function handleLogs(argv) {
|
|
8
|
+
const { options } = parseFlags(argv, LOGS_BOOLEANS);
|
|
9
|
+
const json = flag(options, "json");
|
|
10
|
+
const target = targetOf(options);
|
|
11
|
+
const adapter = resolveAdapter(options, target);
|
|
12
|
+
if (!adapter) {
|
|
13
|
+
return usageOut(json, "logs", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
14
|
+
}
|
|
15
|
+
const surface = getAdapterSurface(adapter);
|
|
16
|
+
if (surface.headless) {
|
|
17
|
+
return usageOut(json, "logs", "core is headless; it has no dev server logs.", surface.hints.launch);
|
|
18
|
+
}
|
|
19
|
+
const logSources = surface.logSources(target);
|
|
20
|
+
const sourceLabels = logSources.map((entry) => entry.label);
|
|
21
|
+
const defaultSource = sourceLabels[0];
|
|
22
|
+
const source = str(options, "source") ?? defaultSource;
|
|
23
|
+
const validSources = [...sourceLabels, "app"];
|
|
24
|
+
if (!validSources.includes(source)) {
|
|
25
|
+
return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
|
|
26
|
+
}
|
|
27
|
+
const events = str(options, "events");
|
|
28
|
+
if (events !== void 0) {
|
|
29
|
+
if (!/^\d+$/u.test(events)) {
|
|
30
|
+
return usageOut(json, "logs", `--events must be numeric (got: ${events}).`, "mm-harness logs --events 20");
|
|
31
|
+
}
|
|
32
|
+
process.env.RECIPE_LOG_EVENTS = events;
|
|
33
|
+
}
|
|
34
|
+
const requested = logSources.find((entry) => entry.label === source);
|
|
35
|
+
const ordered = requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
|
|
36
|
+
const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
|
|
37
|
+
if (!logFile) {
|
|
38
|
+
const names = logSources.map((entry) => path.basename(entry.path)).join(" / ");
|
|
39
|
+
return usageOut(
|
|
40
|
+
json,
|
|
41
|
+
"logs",
|
|
42
|
+
`nothing running for this checkout (no ${names}).`,
|
|
43
|
+
surface.hints.launch
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const full = flag(options, "full");
|
|
47
|
+
if (full) {
|
|
48
|
+
const result2 = spawnScript("tail", ["-n", "+1", "-F", logFile], target, json);
|
|
49
|
+
return result2.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
50
|
+
}
|
|
51
|
+
const logTui = path.join(runnerDir, "adapters/shared/log-tui.mjs");
|
|
52
|
+
const eventCount = process.env.RECIPE_LOG_EVENTS ?? "20";
|
|
53
|
+
const uiMode = process.env.RECIPE_LOG_UI_MODE ?? "compact";
|
|
54
|
+
const result = spawnScript(
|
|
55
|
+
process.execPath,
|
|
56
|
+
[logTui, "tail", "--log", logFile, "--events", eventCount, "--follow", "--mode", uiMode],
|
|
57
|
+
target,
|
|
58
|
+
json
|
|
59
|
+
);
|
|
60
|
+
if (json) {
|
|
61
|
+
console.log(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{ schemaVersion: 1, command: "logs", adapter, source, logFile, exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime },
|
|
64
|
+
null,
|
|
65
|
+
2
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
handleLogs
|
|
73
|
+
};
|