@deeeed/metamask-harness 0.17.5 → 0.18.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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +90 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +2 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -3,13 +3,15 @@ import http from "node:http";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { depsCheck } from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
6
|
+
import { colorHumanMessage } from "../../cli-color.js";
|
|
6
7
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
7
8
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
9
|
+
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
8
10
|
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
9
11
|
import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
|
|
10
12
|
import { ensureExtensionHarnessFresh } from "../../adapters/extension/harness-freshness.js";
|
|
11
13
|
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
12
|
-
import { spawnScriptStreaming } from "../shared.js";
|
|
14
|
+
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
13
15
|
function extensionDepsBlock(target) {
|
|
14
16
|
if (!fs.existsSync(path.join(target, "package.json"))) return null;
|
|
15
17
|
const deps = depsCheck(target);
|
|
@@ -30,18 +32,32 @@ function installExtensionDeps(target) {
|
|
|
30
32
|
return spawnScriptStreaming(installScript, ["--target", target], target);
|
|
31
33
|
}
|
|
32
34
|
async function launchExtension(target, tier, wantWatch, displayMode = "fullscreen") {
|
|
35
|
+
if (process.env.CHROME_USER_DATA_DIR) {
|
|
36
|
+
process.env.CHROME_USER_DATA_DIR = path.resolve(process.env.CHROME_USER_DATA_DIR);
|
|
37
|
+
}
|
|
38
|
+
const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target);
|
|
39
|
+
if (!reusable) {
|
|
40
|
+
const block = extensionProductConfigBlock(target);
|
|
41
|
+
if (block) {
|
|
42
|
+
return {
|
|
43
|
+
status: EXIT.infra,
|
|
44
|
+
output: `EXTENSION_PRODUCT_CONFIG_REQUIRED: ${block.message}
|
|
45
|
+
Next: ${block.userAction}`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
await ensureExtensionHarnessFresh(target);
|
|
34
50
|
if (wantWatch) {
|
|
35
51
|
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
36
52
|
const watchArgs = ["--target", target];
|
|
37
53
|
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
38
|
-
console.error(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
54
|
+
console.error(colorHumanMessage(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`));
|
|
39
55
|
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
40
56
|
}
|
|
41
57
|
if (tier === "build") {
|
|
42
58
|
return extensionRebuild(target);
|
|
43
59
|
}
|
|
44
|
-
if (
|
|
60
|
+
if (reusable) {
|
|
45
61
|
return extensionReattach(target, displayMode);
|
|
46
62
|
}
|
|
47
63
|
return extensionRebuild(target);
|
|
@@ -49,12 +65,16 @@ async function launchExtension(target, tier, wantWatch, displayMode = "fullscree
|
|
|
49
65
|
async function extensionRuntimeReusable(target) {
|
|
50
66
|
const cdpPort = process.env.CDP_PORT;
|
|
51
67
|
if (!cdpPort) return false;
|
|
52
|
-
if (!cdpOwnedByExpectedRuntime(cdpPort, target)) return false;
|
|
53
|
-
if (!await cdpVersionReachable(cdpPort)) return false;
|
|
54
|
-
if (!await cdpHasExpectedExtensionTarget(cdpPort, target)) return false;
|
|
55
|
-
if (!await cdpRuntimeHealthy(cdpPort, target)) return false;
|
|
56
68
|
if (isExtensionDistStale(target)) return false;
|
|
57
|
-
|
|
69
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
70
|
+
const reachable = await cdpVersionReachable(cdpPort);
|
|
71
|
+
const owned = reachable && cdpOwnedByExpectedRuntime(cdpPort, target);
|
|
72
|
+
const targetPresent = owned && await cdpHasExpectedExtensionTarget(cdpPort, target);
|
|
73
|
+
const healthy = targetPresent && await cdpRuntimeHealthy(cdpPort, target);
|
|
74
|
+
if (healthy) return true;
|
|
75
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
58
78
|
}
|
|
59
79
|
function expectedChromeProfile(target) {
|
|
60
80
|
const profileName = process.env.RECIPE_CHROME_PROFILE_NAME || "chrome-profile";
|
|
@@ -170,12 +190,15 @@ function cdpHasExpectedExtensionTarget(port, target) {
|
|
|
170
190
|
});
|
|
171
191
|
}
|
|
172
192
|
async function cdpRuntimeHealthy(port, target) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
193
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
194
|
+
try {
|
|
195
|
+
const report = await checkExtensionRuntimeHealth(target, Number(port), { pageMode: "home-or-sidepanel" });
|
|
196
|
+
if (report.status === "PASS") return true;
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 250));
|
|
178
200
|
}
|
|
201
|
+
return false;
|
|
179
202
|
}
|
|
180
203
|
async function extensionReattach(target, displayMode = "fullscreen") {
|
|
181
204
|
const reattachScript = recipeHarnessPath(target, "extension", "scripts", "reattach.sh");
|
|
@@ -183,7 +206,7 @@ async function extensionReattach(target, displayMode = "fullscreen") {
|
|
|
183
206
|
if (process.env.CDP_PORT) reattachArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
184
207
|
if (process.env.WATCHER_PORT) reattachArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
185
208
|
if (process.env.EXTENSION_START_URL) reattachArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
186
|
-
console.error(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`);
|
|
209
|
+
console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
|
|
187
210
|
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
188
211
|
}
|
|
189
212
|
async function extensionRebuild(target) {
|
|
@@ -197,7 +220,7 @@ async function extensionRebuild(target) {
|
|
|
197
220
|
const liveArgs = ["--target", target, "--start-watch"];
|
|
198
221
|
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
199
222
|
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
200
|
-
console.error(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`);
|
|
223
|
+
console.error(colorHumanMessage(`\u2192 extension quick relaunch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`));
|
|
201
224
|
const result = await spawnScriptStreaming(liveScript, liveArgs, target);
|
|
202
225
|
if (result.output) {
|
|
203
226
|
try {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { color } from "../../cli-color.js";
|
|
4
|
+
import { color, colorHumanMessage } from "../../cli-color.js";
|
|
5
5
|
import { handleHarness } from "../../harness.js";
|
|
6
6
|
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
7
7
|
import { getAdapterSurface } from "../../adapters/surface.js";
|
|
8
8
|
import {
|
|
9
9
|
ADAPTER_DETECT_NEXT,
|
|
10
|
+
checkoutBusyOut,
|
|
10
11
|
EXIT,
|
|
11
12
|
flag,
|
|
12
13
|
parseFlags,
|
|
@@ -14,7 +15,8 @@ import {
|
|
|
14
15
|
spawnScript,
|
|
15
16
|
str,
|
|
16
17
|
targetOf,
|
|
17
|
-
usageOut
|
|
18
|
+
usageOut,
|
|
19
|
+
writeInteractiveProgress
|
|
18
20
|
} from "../shared.js";
|
|
19
21
|
import {
|
|
20
22
|
RECOVERY_CODE,
|
|
@@ -32,6 +34,9 @@ import {
|
|
|
32
34
|
import { launchMobile } from "./mobile.js";
|
|
33
35
|
import { applyDeviceTargeting } from "../device-target.js";
|
|
34
36
|
import { acquireCheckoutLock } from "../../checkout-lock.js";
|
|
37
|
+
import { JsonStreamWriter } from "../../json-stream.js";
|
|
38
|
+
import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
|
|
39
|
+
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
35
40
|
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
36
41
|
"build",
|
|
37
42
|
"watch",
|
|
@@ -39,30 +44,58 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
|
39
44
|
"sidepanel",
|
|
40
45
|
"fullscreen",
|
|
41
46
|
"runway",
|
|
42
|
-
"json"
|
|
47
|
+
"json",
|
|
48
|
+
"jsonStream"
|
|
43
49
|
]);
|
|
44
50
|
const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
|
|
45
51
|
async function handleLaunch(argv) {
|
|
46
52
|
const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
47
53
|
const json = flag(options, "json");
|
|
54
|
+
const stream = new JsonStreamWriter("launch", flag(options, "jsonStream"));
|
|
55
|
+
const jsonOutput = json && !stream.enabled;
|
|
56
|
+
const restoreStdout = stream.isolateStdout();
|
|
48
57
|
const target = targetOf(options);
|
|
49
|
-
if (!fs.existsSync(target)) return handleLaunchLocked(argv);
|
|
50
|
-
const lock = acquireCheckoutLock(target, "launch");
|
|
51
|
-
if ("message" in lock) {
|
|
52
|
-
return usageOut(json, "launch", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
|
|
53
|
-
}
|
|
54
58
|
try {
|
|
55
|
-
|
|
59
|
+
let exitCode;
|
|
60
|
+
if (!fs.existsSync(target)) {
|
|
61
|
+
exitCode = await handleLaunchLocked(argv, stream);
|
|
62
|
+
} else {
|
|
63
|
+
const lock = acquireCheckoutLock(target, "launch");
|
|
64
|
+
if ("message" in lock) {
|
|
65
|
+
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
66
|
+
stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
|
|
67
|
+
exitCode = checkoutBusyOut(jsonOutput, "launch", lock.message, lock.path);
|
|
68
|
+
} else {
|
|
69
|
+
try {
|
|
70
|
+
exitCode = await handleLaunchLocked(argv, stream);
|
|
71
|
+
} finally {
|
|
72
|
+
lock.release();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
77
|
+
return exitCode;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
80
|
+
stream.error({
|
|
81
|
+
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "LAUNCH_FAILED",
|
|
82
|
+
message: error instanceof Error ? error.message : String(error),
|
|
83
|
+
userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
|
|
84
|
+
});
|
|
85
|
+
stream.complete("fail", exitCode);
|
|
86
|
+
throw error;
|
|
56
87
|
} finally {
|
|
57
|
-
|
|
88
|
+
restoreStdout();
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
|
-
async function handleLaunchLocked(argv) {
|
|
91
|
+
async function handleLaunchLocked(argv, stream) {
|
|
61
92
|
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
62
93
|
const json = flag(options, "json");
|
|
94
|
+
const jsonOutput = json && !stream.enabled;
|
|
95
|
+
const machine = json || stream.enabled;
|
|
63
96
|
const target = targetOf(options);
|
|
64
97
|
const heal = parseHeal(options, "auto");
|
|
65
|
-
if (typeof heal !== "string") return
|
|
98
|
+
if (typeof heal !== "string") return launchUsage(jsonOutput, stream, heal.error, "use --heal off|infra-only|auto");
|
|
66
99
|
const posToken = positional[0];
|
|
67
100
|
const mobileTargetToken = posToken === "ios" || posToken === "android" ? posToken : void 0;
|
|
68
101
|
const platformFlag = str(options, "platform");
|
|
@@ -71,15 +104,16 @@ async function handleLaunchLocked(argv) {
|
|
|
71
104
|
const adapterHint = mobileTarget ? "mobile" : void 0;
|
|
72
105
|
const adapter = resolveAdapter(options, target, adapterHint);
|
|
73
106
|
if (!adapter) {
|
|
74
|
-
return
|
|
107
|
+
return launchUsage(jsonOutput, stream, `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
75
108
|
}
|
|
109
|
+
stream.phase("resolve", { target, adapter, platform: mobileTarget ?? null });
|
|
76
110
|
if (adapter === "core") {
|
|
77
|
-
return
|
|
111
|
+
return launchUsage(jsonOutput, stream, "core is headless; there is nothing to launch.", "mm-harness verify");
|
|
78
112
|
}
|
|
79
113
|
if (adapter === "mobile" && !mobileTarget) {
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
114
|
+
return launchUsage(
|
|
115
|
+
jsonOutput,
|
|
116
|
+
stream,
|
|
83
117
|
"target is required for mobile.",
|
|
84
118
|
"mm-harness launch ios or mm-harness launch android"
|
|
85
119
|
);
|
|
@@ -89,15 +123,15 @@ async function handleLaunchLocked(argv) {
|
|
|
89
123
|
const wantWatch = flag(options, "watch");
|
|
90
124
|
const wantRunway = flag(options, "runway");
|
|
91
125
|
if (wantRunway && adapter !== "mobile") {
|
|
92
|
-
return
|
|
126
|
+
return launchUsage(jsonOutput, stream, "runway is mobile-only.", "drop --runway for the extension");
|
|
93
127
|
}
|
|
94
128
|
const displayMode = flag(options, "sidepanel") && !flag(options, "fullscreen") ? "sidepanel" : "fullscreen";
|
|
95
129
|
for (const portFlag of ["cdpPort", "watcherPort"]) {
|
|
96
130
|
const value = str(options, portFlag);
|
|
97
131
|
if (value !== void 0 && !/^\d+$/u.test(value)) {
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
return launchUsage(
|
|
133
|
+
jsonOutput,
|
|
134
|
+
stream,
|
|
101
135
|
`--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
|
|
102
136
|
`pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
|
|
103
137
|
);
|
|
@@ -105,7 +139,7 @@ async function handleLaunchLocked(argv) {
|
|
|
105
139
|
}
|
|
106
140
|
const envResult = applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
107
141
|
if (envResult && "code" in envResult) {
|
|
108
|
-
return
|
|
142
|
+
return launchUsage(jsonOutput, stream, envResult.message, envResult.userAction);
|
|
109
143
|
}
|
|
110
144
|
const tier = wantBuild ? "build" : "quick";
|
|
111
145
|
if (adapter === "extension") {
|
|
@@ -114,44 +148,65 @@ async function handleLaunchLocked(argv) {
|
|
|
114
148
|
process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
|
|
115
149
|
}
|
|
116
150
|
}
|
|
117
|
-
if (!
|
|
151
|
+
if (!machine && adapter === "extension") {
|
|
118
152
|
const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
|
|
119
153
|
const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
|
|
120
|
-
console.error(
|
|
154
|
+
console.error(colorHumanMessage(
|
|
121
155
|
`\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
if (adapter === "mobile") {
|
|
159
|
+
const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
|
|
160
|
+
const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
|
|
161
|
+
writeInteractiveProgress(
|
|
162
|
+
machine,
|
|
163
|
+
`\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}`
|
|
122
164
|
);
|
|
123
165
|
}
|
|
124
166
|
const state = newHealState();
|
|
125
167
|
if (tier === "quick" && nativeInputsChanged(target, adapter)) {
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
|
|
168
|
+
return launchUsage(
|
|
169
|
+
jsonOutput,
|
|
170
|
+
stream,
|
|
129
171
|
"native build inputs changed since last build.",
|
|
130
172
|
`mm-harness launch ${adapter === "mobile" ? `${mobileTarget} ` : ""}--build`
|
|
131
173
|
);
|
|
132
174
|
}
|
|
133
|
-
|
|
175
|
+
stream.phase("install");
|
|
176
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, machine);
|
|
134
177
|
if (!ensured.ok) {
|
|
135
|
-
return launchFail(
|
|
178
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
136
179
|
code: "OVERLAY_INSTALL_FAILED",
|
|
137
180
|
message: ensured.error ?? "overlay install failed",
|
|
138
181
|
recoverable: false,
|
|
139
182
|
exitCode: EXIT.infra
|
|
140
183
|
});
|
|
141
184
|
}
|
|
185
|
+
stream.phase("healthcheck");
|
|
142
186
|
if (adapter === "extension") {
|
|
143
187
|
const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
|
|
144
188
|
if (willBuild) {
|
|
189
|
+
const productConfigBlock = extensionProductConfigBlock(target);
|
|
190
|
+
if (productConfigBlock) {
|
|
191
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
192
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
193
|
+
message: productConfigBlock.message,
|
|
194
|
+
recoverable: false,
|
|
195
|
+
userAction: productConfigBlock.userAction,
|
|
196
|
+
exitCode: EXIT.infra
|
|
197
|
+
});
|
|
198
|
+
}
|
|
145
199
|
const depsBlock = extensionDepsBlock(target);
|
|
146
200
|
if (depsBlock) {
|
|
147
201
|
if (heal === "off") {
|
|
148
|
-
return
|
|
202
|
+
return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
|
|
149
203
|
}
|
|
150
204
|
const recoveryCode2 = "deps.installed";
|
|
151
205
|
state.attemptedRecoveries.push(recoveryCode2);
|
|
206
|
+
stream.phase("recover");
|
|
152
207
|
const repaired = await installExtensionDeps(target);
|
|
153
208
|
if (repaired.status !== 0 || extensionDepsBlock(target)) {
|
|
154
|
-
return launchFail(
|
|
209
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
155
210
|
code: "DEPENDENCY_INSTALL_FAILED",
|
|
156
211
|
message: depsBlock.message,
|
|
157
212
|
recoverable: false,
|
|
@@ -165,12 +220,32 @@ async function handleLaunchLocked(argv) {
|
|
|
165
220
|
}
|
|
166
221
|
}
|
|
167
222
|
}
|
|
168
|
-
|
|
223
|
+
stream.phase("launch");
|
|
224
|
+
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
|
|
169
225
|
if (attempt.status === 0) {
|
|
170
|
-
return await finishLaunch(
|
|
226
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
227
|
+
}
|
|
228
|
+
if (adapter === "extension" && extensionProductConfigMissing(attempt.output)) {
|
|
229
|
+
const block = extensionProductConfigBlock(target);
|
|
230
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
231
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
232
|
+
message: block?.message ?? "Extension product configuration was not ready when the build started.",
|
|
233
|
+
recoverable: false,
|
|
234
|
+
userAction: block?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
235
|
+
exitCode: EXIT.infra
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (adapter === "extension" && extensionBrowserMissing(attempt.output)) {
|
|
239
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
240
|
+
code: "EXTENSION_BROWSER_REQUIRED",
|
|
241
|
+
message: "the checkout Playwright browser is not installed; system Chrome is not a compatible unpacked-extension fallback. Installing the browser cache requires user approval.",
|
|
242
|
+
recoverable: false,
|
|
243
|
+
userAction: `ask the user for approval, then run: cd ${shellQuote(target)} && yarn playwright install chromium`,
|
|
244
|
+
exitCode: EXIT.infra
|
|
245
|
+
});
|
|
171
246
|
}
|
|
172
247
|
if (heal === "off") {
|
|
173
|
-
return launchFail(
|
|
248
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
174
249
|
code: "LAUNCH_FAILED",
|
|
175
250
|
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed (healing off)`,
|
|
176
251
|
recoverable: false,
|
|
@@ -180,12 +255,13 @@ async function handleLaunchLocked(argv) {
|
|
|
180
255
|
if (adapter === "extension" && tier === "quick" && extensionQuickReattachFailed(attempt.output)) {
|
|
181
256
|
const recoveryCode2 = "chrome.relaunched";
|
|
182
257
|
state.attemptedRecoveries.push(recoveryCode2);
|
|
183
|
-
|
|
258
|
+
stream.phase("recover");
|
|
259
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
184
260
|
if (rebuildAttempt.status === 0) {
|
|
185
261
|
state.recovered.push(recoveryCode2);
|
|
186
|
-
return await finishLaunch(
|
|
262
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
187
263
|
}
|
|
188
|
-
return launchFail(
|
|
264
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
189
265
|
code: "EXTENSION_RELAUNCH_FAILED",
|
|
190
266
|
message: "quick reattach failed, and clean relaunch failed too.",
|
|
191
267
|
recoverable: false,
|
|
@@ -194,7 +270,7 @@ async function handleLaunchLocked(argv) {
|
|
|
194
270
|
});
|
|
195
271
|
}
|
|
196
272
|
if (adapter === "mobile" && mobileProvisioningBlocked(attempt.output)) {
|
|
197
|
-
return launchFail(
|
|
273
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
198
274
|
code: "MOBILE_PROVISION_REQUIRED",
|
|
199
275
|
message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
|
|
200
276
|
recoverable: false,
|
|
@@ -205,7 +281,7 @@ async function handleLaunchLocked(argv) {
|
|
|
205
281
|
}
|
|
206
282
|
const bound = checkHealBounds(target, attempt.output, state);
|
|
207
283
|
if (bound !== null) {
|
|
208
|
-
return launchFail(
|
|
284
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
209
285
|
code: bound.code,
|
|
210
286
|
message: bound.message,
|
|
211
287
|
recoverable: false,
|
|
@@ -216,12 +292,13 @@ async function handleLaunchLocked(argv) {
|
|
|
216
292
|
}
|
|
217
293
|
const recoveryCode = RECOVERY_CODE[adapter];
|
|
218
294
|
state.attemptedRecoveries.push(recoveryCode);
|
|
219
|
-
|
|
295
|
+
stream.phase("recover");
|
|
296
|
+
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
|
|
220
297
|
if (attempt.status === 0) {
|
|
221
298
|
state.recovered.push(recoveryCode);
|
|
222
|
-
return await finishLaunch(
|
|
299
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
223
300
|
}
|
|
224
|
-
return launchFail(
|
|
301
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
225
302
|
code: "SAME_RECOVERY_TWICE",
|
|
226
303
|
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
|
|
227
304
|
recoverable: false,
|
|
@@ -235,6 +312,12 @@ function extensionQuickReattachFailed(output) {
|
|
|
235
312
|
function extensionRuntimeBlocked(output) {
|
|
236
313
|
return /ERR_BLOCKED_BY_CLIENT|has been blocked by Chromium/u.test(output);
|
|
237
314
|
}
|
|
315
|
+
function extensionBrowserMissing(output) {
|
|
316
|
+
return /Playwright Chromium is not installed|No compatible Chromium binary selected/u.test(output);
|
|
317
|
+
}
|
|
318
|
+
function extensionProductConfigMissing(output) {
|
|
319
|
+
return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
|
|
320
|
+
}
|
|
238
321
|
function mobileProvisioningBlocked(output) {
|
|
239
322
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
240
323
|
}
|
|
@@ -274,14 +357,16 @@ function applyLaunchEnvOverrides(options, adapter, mobileTarget, target) {
|
|
|
274
357
|
return {
|
|
275
358
|
ok: false,
|
|
276
359
|
code: "DEVICE_WRONG_PLATFORM",
|
|
277
|
-
message: `--device ${device} did not resolve to an Android device for launch android
|
|
360
|
+
message: `--device ${device} did not resolve to an Android device for launch android.`,
|
|
361
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
278
362
|
};
|
|
279
363
|
}
|
|
280
364
|
if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
|
|
281
365
|
return {
|
|
282
366
|
ok: false,
|
|
283
367
|
code: "DEVICE_WRONG_PLATFORM",
|
|
284
|
-
message: `--device ${device} did not resolve to an iOS simulator for launch ios
|
|
368
|
+
message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
|
|
369
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
285
370
|
};
|
|
286
371
|
}
|
|
287
372
|
}
|
|
@@ -327,26 +412,29 @@ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target
|
|
|
327
412
|
}
|
|
328
413
|
return launchExtension(target, tier, wantWatch, displayMode);
|
|
329
414
|
}
|
|
330
|
-
async function finishLaunch(
|
|
415
|
+
async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true) {
|
|
331
416
|
if (adapter === "extension" && displayMode === "sidepanel") {
|
|
332
417
|
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
333
418
|
const sidepanelArgs = ["open"];
|
|
334
419
|
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
335
|
-
if (!
|
|
420
|
+
if (!machine) {
|
|
336
421
|
console.error(
|
|
337
422
|
`[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
|
|
338
423
|
);
|
|
339
424
|
}
|
|
340
|
-
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target,
|
|
425
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
|
|
341
426
|
if (sidepanel.status !== 0) {
|
|
342
427
|
const recoveryCode = "chrome.relaunched";
|
|
343
428
|
if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
|
|
344
429
|
state.attemptedRecoveries.push(recoveryCode);
|
|
345
|
-
|
|
430
|
+
stream.phase("recover");
|
|
431
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
346
432
|
if (rebuildAttempt.status === 0) {
|
|
347
433
|
state.recovered.push(recoveryCode);
|
|
348
434
|
return await finishLaunch(
|
|
349
|
-
|
|
435
|
+
jsonOutput,
|
|
436
|
+
machine,
|
|
437
|
+
stream,
|
|
350
438
|
adapter,
|
|
351
439
|
mobileTarget,
|
|
352
440
|
tier,
|
|
@@ -358,7 +446,7 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
358
446
|
false
|
|
359
447
|
);
|
|
360
448
|
}
|
|
361
|
-
return launchFail(
|
|
449
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
362
450
|
code: "EXTENSION_RELAUNCH_FAILED",
|
|
363
451
|
message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
|
|
364
452
|
recoverable: false,
|
|
@@ -366,7 +454,7 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
366
454
|
originalError: rebuildAttempt.output.trim() || void 0
|
|
367
455
|
});
|
|
368
456
|
}
|
|
369
|
-
return launchFail(
|
|
457
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
370
458
|
code: "SIDEPANEL_OPEN_FAILED",
|
|
371
459
|
message: "app launched but opening the side panel failed.",
|
|
372
460
|
recoverable: false,
|
|
@@ -376,16 +464,48 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
376
464
|
});
|
|
377
465
|
}
|
|
378
466
|
}
|
|
467
|
+
if (adapter === "extension") {
|
|
468
|
+
try {
|
|
469
|
+
await ensureExtensionConsoleCapture(target);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (!machine) {
|
|
472
|
+
console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
379
476
|
if (wantVerify) {
|
|
477
|
+
stream.phase("verify");
|
|
380
478
|
const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
|
|
381
479
|
if (adapter === "mobile" && mobileTarget) verifyArgs.push("--platform", mobileTarget);
|
|
382
|
-
if (
|
|
383
|
-
|
|
480
|
+
if (machine) verifyArgs.push("--json");
|
|
481
|
+
const exitCode = await handleHarness(verifyArgs);
|
|
482
|
+
if (stream.enabled) {
|
|
483
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
484
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
485
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
|
|
486
|
+
adapter,
|
|
487
|
+
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
488
|
+
tier,
|
|
489
|
+
recovered: state.recovered,
|
|
490
|
+
mutations: state.mutations
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return exitCode;
|
|
384
494
|
}
|
|
385
|
-
return launchPass(
|
|
495
|
+
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
|
|
386
496
|
}
|
|
387
|
-
function launchPass(json, adapter, mobileTarget, tier, displayMode, state) {
|
|
388
|
-
if (
|
|
497
|
+
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
|
|
498
|
+
if (stream.enabled) {
|
|
499
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
500
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
501
|
+
stream.complete("pass", EXIT.ok, {
|
|
502
|
+
adapter,
|
|
503
|
+
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
504
|
+
tier,
|
|
505
|
+
recovered: state.recovered,
|
|
506
|
+
mutations: state.mutations
|
|
507
|
+
});
|
|
508
|
+
} else if (json) {
|
|
389
509
|
console.log(
|
|
390
510
|
JSON.stringify(
|
|
391
511
|
{
|
|
@@ -423,8 +543,32 @@ function launchDeviceLabel(mobileTarget) {
|
|
|
423
543
|
}
|
|
424
544
|
return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
|
|
425
545
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
546
|
+
function launchUsage(json, stream, message, userAction) {
|
|
547
|
+
stream.error({ code: "USAGE", message, userAction });
|
|
548
|
+
return usageOut(json, "launch", message, userAction);
|
|
549
|
+
}
|
|
550
|
+
function launchFail(json, stream, adapter, mobileTarget, tier, state, target, failure) {
|
|
551
|
+
const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
552
|
+
if (stream.enabled) {
|
|
553
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
554
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
555
|
+
stream.error({
|
|
556
|
+
code: failure.code,
|
|
557
|
+
message: failure.message,
|
|
558
|
+
retryable: failure.recoverable,
|
|
559
|
+
userAction,
|
|
560
|
+
originalError: failure.originalError ?? null
|
|
561
|
+
});
|
|
562
|
+
stream.complete("fail", failure.exitCode, {
|
|
563
|
+
adapter,
|
|
564
|
+
platform: mobileTarget ?? null,
|
|
565
|
+
tier,
|
|
566
|
+
recovered: state.recovered,
|
|
567
|
+
mutations: state.mutations,
|
|
568
|
+
recoverable: failure.recoverable,
|
|
569
|
+
attemptedRecoveries: state.attemptedRecoveries
|
|
570
|
+
});
|
|
571
|
+
} else if (json) {
|
|
428
572
|
console.log(
|
|
429
573
|
JSON.stringify(
|
|
430
574
|
{
|
|
@@ -444,7 +588,7 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
|
444
588
|
code: failure.code,
|
|
445
589
|
message: failure.message,
|
|
446
590
|
retryable: failure.recoverable,
|
|
447
|
-
userAction
|
|
591
|
+
userAction,
|
|
448
592
|
originalError: failure.originalError ?? null
|
|
449
593
|
}
|
|
450
594
|
},
|
|
@@ -453,13 +597,13 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
|
453
597
|
)
|
|
454
598
|
);
|
|
455
599
|
} else {
|
|
456
|
-
console.error(
|
|
600
|
+
console.error(colorHumanMessage(
|
|
457
601
|
`\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
|
|
458
602
|
${failure.message}` + (failure.originalError ? `
|
|
459
603
|
--- original failure ---
|
|
460
|
-
${failure.originalError}` : "") +
|
|
461
|
-
Next: ${
|
|
462
|
-
);
|
|
604
|
+
${failure.originalError}` : "") + `
|
|
605
|
+
Next: ${userAction}`
|
|
606
|
+
));
|
|
463
607
|
}
|
|
464
608
|
return failure.exitCode;
|
|
465
609
|
}
|