@deeeed/metamask-harness 0.16.0 → 0.17.1
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 +44 -0
- package/README.md +90 -137
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/console-tail.mjs +51 -24
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +5 -9
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/stamp-runtime-title.cjs +49 -0
- package/adapters/extension/start-watch.sh +16 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +105 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/manifest.json +46 -38
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +88 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +4 -61
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +90 -36
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -5
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/adapters/shared/tmux-viewer.sh +0 -0
- package/dist/adapters/extension/console-capture.js +84 -0
- package/dist/adapters/extension/runtime-decision.js +14 -2
- package/dist/adapters/extension/surface.js +3 -2
- package/dist/adapters/mobile/prepare.js +22 -7
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +0 -2
- package/dist/adapters/slot-ports.js +8 -0
- package/dist/cli.js +5 -11
- package/dist/command-contract.js +441 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +43 -20
- package/dist/commands/check.js +9 -3
- package/dist/commands/debug.js +38 -31
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +19 -6
- package/dist/commands/fixtures.js +19 -11
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/index.js +166 -59
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +114 -29
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +27 -42
- package/dist/commands/run-report.js +1 -1
- package/dist/commands/run.js +243 -41
- package/dist/commands/shared.js +11 -1
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +7 -2
- package/dist/doctor.js +1 -4
- package/dist/harness.js +19 -125
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +6 -20
- package/dist/mm-harness-cli.js +111 -41
- package/dist/paths.js +2 -5
- package/dist/run-diagnostics.js +35 -40
- package/dist/runner.js +32 -1
- package/dist/runtime-context.js +26 -8
- package/docs/CHEATSHEET.md +39 -52
- package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/UX-PRINCIPLES.md +2 -0
- package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
- package/docs/architecture.md +65 -375
- package/docs/live-adapter-contract.md +3 -1
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/manifests/mobile.action-manifest.json +1 -1
- package/package.json +3 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -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/CLI-SPEC.md +0 -1102
- package/docs/MENTAL-MODEL.md +0 -295
package/dist/commands/doctor.js
CHANGED
|
@@ -81,7 +81,7 @@ async function handleDoctor({ options }) {
|
|
|
81
81
|
surface.resolveSlotPorts(target);
|
|
82
82
|
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
83
83
|
if ("code" in dtResult) {
|
|
84
|
-
return usageOut(json, "doctor", dtResult.message,
|
|
84
|
+
return usageOut(json, "doctor", dtResult.message, dtResult.userAction);
|
|
85
85
|
}
|
|
86
86
|
applyDoctorRuntimePorts(options);
|
|
87
87
|
const lock = acquireCheckoutLock(target, "doctor-fix");
|
|
@@ -98,7 +98,14 @@ async function handleDoctor({ options }) {
|
|
|
98
98
|
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
99
99
|
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
|
|
100
100
|
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
101
|
-
|
|
101
|
+
const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
102
|
+
const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
|
|
103
|
+
const error = status === "fail" ? {
|
|
104
|
+
code: "DOCTOR_FIX_INCOMPLETE",
|
|
105
|
+
message: "doctor --fix could not make every required check pass",
|
|
106
|
+
userAction: fixUserAction
|
|
107
|
+
} : void 0;
|
|
108
|
+
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions, ...error ? { error } : {} }, null, 2));
|
|
102
109
|
else {
|
|
103
110
|
console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
|
|
104
111
|
for (const action of nextActions) console.error(` Next: ${action}`);
|
|
@@ -113,8 +120,9 @@ async function handleDoctor({ options }) {
|
|
|
113
120
|
surface.resolveSlotPorts(target);
|
|
114
121
|
const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
|
|
115
122
|
if ("code" in dtResult) {
|
|
116
|
-
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
|
|
117
|
-
else console.error(`\u2717 doctor: ${dtResult.message}
|
|
123
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
|
|
124
|
+
else console.error(`\u2717 doctor: ${dtResult.message}
|
|
125
|
+
Next: ${dtResult.userAction}`);
|
|
118
126
|
return EXIT.usage;
|
|
119
127
|
}
|
|
120
128
|
applyDoctorRuntimePorts(options);
|
|
@@ -151,7 +159,9 @@ async function handleDoctor({ options }) {
|
|
|
151
159
|
printReady
|
|
152
160
|
);
|
|
153
161
|
}
|
|
154
|
-
|
|
162
|
+
const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
163
|
+
const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
|
|
164
|
+
if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...doctorError ? { error: doctorError } : {} }, null, 2));
|
|
155
165
|
else {
|
|
156
166
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
157
167
|
const stateStyle = (value, good) => value === good ? "ok" : "warn";
|
|
@@ -194,6 +204,7 @@ async function handleDoctor({ options }) {
|
|
|
194
204
|
if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
|
|
195
205
|
if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
|
|
196
206
|
}
|
|
207
|
+
if (doctorError) console.log(` ${out("dim", `Next: ${doctorUserAction}`)}`);
|
|
197
208
|
}
|
|
198
209
|
return doctorResult.status === "pass" ? 0 : 1;
|
|
199
210
|
}
|
|
@@ -256,7 +267,9 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
|
|
|
256
267
|
return EXIT.runtime;
|
|
257
268
|
}
|
|
258
269
|
if (json) {
|
|
259
|
-
|
|
270
|
+
const userAction = adapter === "extension" ? `mm-harness launch --adapter extension --target ${shellQuote(target)}` : adapter === "core" ? `mm-harness verify --adapter core --target ${shellQuote(target)}` : `mm-harness status --target ${shellQuote(target)} --json`;
|
|
271
|
+
const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
|
|
272
|
+
console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
|
|
260
273
|
return live ? EXIT.ok : EXIT.runtime;
|
|
261
274
|
}
|
|
262
275
|
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
@@ -56,6 +56,14 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
56
56
|
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
57
57
|
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
58
58
|
}
|
|
59
|
+
if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
|
|
60
|
+
return usageOut(
|
|
61
|
+
json,
|
|
62
|
+
"fixtures",
|
|
63
|
+
"--dev, --from, and --force belong to fixtures init; they do not apply a fixture.",
|
|
64
|
+
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
59
67
|
const target = targetOf(options);
|
|
60
68
|
const adapter = resolveAdapter(options, target);
|
|
61
69
|
if (!adapter) {
|
|
@@ -65,7 +73,7 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
65
73
|
surface.resolveSlotPorts(target);
|
|
66
74
|
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
|
|
67
75
|
if ("code" in dtResult) {
|
|
68
|
-
return usageOut(json, "fixtures", dtResult.message,
|
|
76
|
+
return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
|
|
69
77
|
}
|
|
70
78
|
if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
|
|
71
79
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
@@ -96,6 +104,14 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
96
104
|
return exitCode;
|
|
97
105
|
}
|
|
98
106
|
const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
|
|
107
|
+
if (!fs.existsSync(fixturePath)) {
|
|
108
|
+
return usageOut(
|
|
109
|
+
json,
|
|
110
|
+
"fixtures",
|
|
111
|
+
`no wallet fixture at ${fixturePath}.`,
|
|
112
|
+
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
99
115
|
process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
|
|
100
116
|
`);
|
|
101
117
|
let status;
|
|
@@ -129,14 +145,6 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
129
145
|
console.error(` Next: ${retryHint}`);
|
|
130
146
|
}
|
|
131
147
|
} else {
|
|
132
|
-
if (!fs.existsSync(fixturePath)) {
|
|
133
|
-
return usageOut(
|
|
134
|
-
json,
|
|
135
|
-
"fixtures",
|
|
136
|
-
`no wallet fixture at ${fixturePath}.`,
|
|
137
|
-
"create it (or pass --fixture <path>), then re-run: mm-harness fixtures set"
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
148
|
const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
|
|
141
149
|
process.env.RECIPE_WALLET_FIXTURE = fixturePath;
|
|
142
150
|
try {
|
|
@@ -150,7 +158,7 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
150
158
|
console.error(` Next: ${retryHint}`);
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
|
-
const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}
|
|
161
|
+
const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
|
|
154
162
|
if (json) {
|
|
155
163
|
console.log(
|
|
156
164
|
JSON.stringify(
|
|
@@ -174,7 +182,7 @@ async function handleFixturesLocked(argv, deps) {
|
|
|
174
182
|
2
|
|
175
183
|
)
|
|
176
184
|
);
|
|
177
|
-
} else {
|
|
185
|
+
} else if (status === "pass") {
|
|
178
186
|
console.error(teaching);
|
|
179
187
|
}
|
|
180
188
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readCommandJournal } from "../command-journal.js";
|
|
2
|
+
import { EXIT } from "./shared.js";
|
|
3
|
+
import {
|
|
4
|
+
optionFlag,
|
|
5
|
+
optionString,
|
|
6
|
+
targetPath
|
|
7
|
+
} from "./parse-args.js";
|
|
8
|
+
async function handleLast({ options }) {
|
|
9
|
+
const target = targetPath(options);
|
|
10
|
+
const { file, record } = readCommandJournal(target, optionString(options, "runtimeDir"));
|
|
11
|
+
const json = optionFlag(options, "json");
|
|
12
|
+
if (!record) {
|
|
13
|
+
const message = `no resumability journal exists for ${target}`;
|
|
14
|
+
const userAction = "run a proof or runtime command in this checkout, then re-run mm-harness last --json";
|
|
15
|
+
if (json) {
|
|
16
|
+
console.log(JSON.stringify({
|
|
17
|
+
schemaVersion: 1,
|
|
18
|
+
command: "last",
|
|
19
|
+
target,
|
|
20
|
+
journalPath: file,
|
|
21
|
+
status: "fail",
|
|
22
|
+
exitCode: EXIT.runtime,
|
|
23
|
+
error: { code: "LAST_NOT_FOUND", message, userAction }
|
|
24
|
+
}, null, 2));
|
|
25
|
+
} else {
|
|
26
|
+
console.error(`\u2717 mm-harness last: ${message}
|
|
27
|
+
Next: ${userAction}`);
|
|
28
|
+
}
|
|
29
|
+
return EXIT.runtime;
|
|
30
|
+
}
|
|
31
|
+
if (json) {
|
|
32
|
+
console.log(JSON.stringify({
|
|
33
|
+
schemaVersion: 1,
|
|
34
|
+
command: "last",
|
|
35
|
+
target,
|
|
36
|
+
journalPath: file,
|
|
37
|
+
status: "pass",
|
|
38
|
+
exitCode: EXIT.ok,
|
|
39
|
+
last: record
|
|
40
|
+
}, null, 2));
|
|
41
|
+
} else {
|
|
42
|
+
const end = record.finishedAt ?? "still running or interrupted";
|
|
43
|
+
console.log(`${record.verdict.toUpperCase()} ${record.command} (exit ${record.exitCode ?? "pending"})`);
|
|
44
|
+
console.log(`started: ${record.startedAt}`);
|
|
45
|
+
console.log(`finished: ${end}`);
|
|
46
|
+
if (record.evidencePaths.length > 0) console.log(`evidence: ${record.evidencePaths.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
return EXIT.ok;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
handleLast
|
|
52
|
+
};
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
spawnScript,
|
|
16
16
|
str,
|
|
17
17
|
targetOf,
|
|
18
|
-
usageOut
|
|
18
|
+
usageOut,
|
|
19
|
+
writeInteractiveProgress
|
|
19
20
|
} from "../shared.js";
|
|
20
21
|
import {
|
|
21
22
|
RECOVERY_CODE,
|
|
@@ -33,6 +34,8 @@ import {
|
|
|
33
34
|
import { launchMobile } from "./mobile.js";
|
|
34
35
|
import { applyDeviceTargeting } from "../device-target.js";
|
|
35
36
|
import { acquireCheckoutLock } from "../../checkout-lock.js";
|
|
37
|
+
import { JsonStreamWriter } from "../../json-stream.js";
|
|
38
|
+
import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
|
|
36
39
|
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
37
40
|
"build",
|
|
38
41
|
"watch",
|
|
@@ -40,30 +43,58 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
|
40
43
|
"sidepanel",
|
|
41
44
|
"fullscreen",
|
|
42
45
|
"runway",
|
|
43
|
-
"json"
|
|
46
|
+
"json",
|
|
47
|
+
"jsonStream"
|
|
44
48
|
]);
|
|
45
49
|
const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
|
|
46
50
|
async function handleLaunch(argv) {
|
|
47
51
|
const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
48
52
|
const json = flag(options, "json");
|
|
53
|
+
const stream = new JsonStreamWriter("launch", flag(options, "jsonStream"));
|
|
54
|
+
const jsonOutput = json && !stream.enabled;
|
|
55
|
+
const restoreStdout = stream.isolateStdout();
|
|
49
56
|
const target = targetOf(options);
|
|
50
|
-
if (!fs.existsSync(target)) return handleLaunchLocked(argv);
|
|
51
|
-
const lock = acquireCheckoutLock(target, "launch");
|
|
52
|
-
if ("message" in lock) {
|
|
53
|
-
return checkoutBusyOut(json, "launch", lock.message, lock.path);
|
|
54
|
-
}
|
|
55
57
|
try {
|
|
56
|
-
|
|
58
|
+
let exitCode;
|
|
59
|
+
if (!fs.existsSync(target)) {
|
|
60
|
+
exitCode = await handleLaunchLocked(argv, stream);
|
|
61
|
+
} else {
|
|
62
|
+
const lock = acquireCheckoutLock(target, "launch");
|
|
63
|
+
if ("message" in lock) {
|
|
64
|
+
const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
|
|
65
|
+
stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
|
|
66
|
+
exitCode = checkoutBusyOut(jsonOutput, "launch", lock.message, lock.path);
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
exitCode = await handleLaunchLocked(argv, stream);
|
|
70
|
+
} finally {
|
|
71
|
+
lock.release();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
|
|
76
|
+
return exitCode;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
|
|
79
|
+
stream.error({
|
|
80
|
+
code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "LAUNCH_FAILED",
|
|
81
|
+
message: error instanceof Error ? error.message : String(error),
|
|
82
|
+
userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
|
|
83
|
+
});
|
|
84
|
+
stream.complete("fail", exitCode);
|
|
85
|
+
throw error;
|
|
57
86
|
} finally {
|
|
58
|
-
|
|
87
|
+
restoreStdout();
|
|
59
88
|
}
|
|
60
89
|
}
|
|
61
|
-
async function handleLaunchLocked(argv) {
|
|
90
|
+
async function handleLaunchLocked(argv, stream) {
|
|
62
91
|
const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
|
|
63
92
|
const json = flag(options, "json");
|
|
93
|
+
const jsonOutput = json && !stream.enabled;
|
|
94
|
+
const machine = json || stream.enabled;
|
|
64
95
|
const target = targetOf(options);
|
|
65
96
|
const heal = parseHeal(options, "auto");
|
|
66
|
-
if (typeof heal !== "string") return
|
|
97
|
+
if (typeof heal !== "string") return launchUsage(jsonOutput, stream, heal.error, "use --heal off|infra-only|auto");
|
|
67
98
|
const posToken = positional[0];
|
|
68
99
|
const mobileTargetToken = posToken === "ios" || posToken === "android" ? posToken : void 0;
|
|
69
100
|
const platformFlag = str(options, "platform");
|
|
@@ -72,15 +103,16 @@ async function handleLaunchLocked(argv) {
|
|
|
72
103
|
const adapterHint = mobileTarget ? "mobile" : void 0;
|
|
73
104
|
const adapter = resolveAdapter(options, target, adapterHint);
|
|
74
105
|
if (!adapter) {
|
|
75
|
-
return
|
|
106
|
+
return launchUsage(jsonOutput, stream, `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
|
|
76
107
|
}
|
|
108
|
+
stream.phase("resolve", { target, adapter, platform: mobileTarget ?? null });
|
|
77
109
|
if (adapter === "core") {
|
|
78
|
-
return
|
|
110
|
+
return launchUsage(jsonOutput, stream, "core is headless; there is nothing to launch.", "mm-harness verify");
|
|
79
111
|
}
|
|
80
112
|
if (adapter === "mobile" && !mobileTarget) {
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
113
|
+
return launchUsage(
|
|
114
|
+
jsonOutput,
|
|
115
|
+
stream,
|
|
84
116
|
"target is required for mobile.",
|
|
85
117
|
"mm-harness launch ios or mm-harness launch android"
|
|
86
118
|
);
|
|
@@ -90,15 +122,15 @@ async function handleLaunchLocked(argv) {
|
|
|
90
122
|
const wantWatch = flag(options, "watch");
|
|
91
123
|
const wantRunway = flag(options, "runway");
|
|
92
124
|
if (wantRunway && adapter !== "mobile") {
|
|
93
|
-
return
|
|
125
|
+
return launchUsage(jsonOutput, stream, "runway is mobile-only.", "drop --runway for the extension");
|
|
94
126
|
}
|
|
95
127
|
const displayMode = flag(options, "sidepanel") && !flag(options, "fullscreen") ? "sidepanel" : "fullscreen";
|
|
96
128
|
for (const portFlag of ["cdpPort", "watcherPort"]) {
|
|
97
129
|
const value = str(options, portFlag);
|
|
98
130
|
if (value !== void 0 && !/^\d+$/u.test(value)) {
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
return launchUsage(
|
|
132
|
+
jsonOutput,
|
|
133
|
+
stream,
|
|
102
134
|
`--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
|
|
103
135
|
`pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
|
|
104
136
|
);
|
|
@@ -106,7 +138,7 @@ async function handleLaunchLocked(argv) {
|
|
|
106
138
|
}
|
|
107
139
|
const envResult = applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
|
|
108
140
|
if (envResult && "code" in envResult) {
|
|
109
|
-
return
|
|
141
|
+
return launchUsage(jsonOutput, stream, envResult.message, envResult.userAction);
|
|
110
142
|
}
|
|
111
143
|
const tier = wantBuild ? "build" : "quick";
|
|
112
144
|
if (adapter === "extension") {
|
|
@@ -115,44 +147,55 @@ async function handleLaunchLocked(argv) {
|
|
|
115
147
|
process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
|
|
116
148
|
}
|
|
117
149
|
}
|
|
118
|
-
if (!
|
|
150
|
+
if (!machine && adapter === "extension") {
|
|
119
151
|
const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
|
|
120
152
|
const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
|
|
121
153
|
console.error(
|
|
122
154
|
`\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
|
|
123
155
|
);
|
|
124
156
|
}
|
|
157
|
+
if (adapter === "mobile") {
|
|
158
|
+
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";
|
|
159
|
+
const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
|
|
160
|
+
writeInteractiveProgress(
|
|
161
|
+
machine,
|
|
162
|
+
`\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
125
165
|
const state = newHealState();
|
|
126
166
|
if (tier === "quick" && nativeInputsChanged(target, adapter)) {
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
return launchUsage(
|
|
168
|
+
jsonOutput,
|
|
169
|
+
stream,
|
|
130
170
|
"native build inputs changed since last build.",
|
|
131
171
|
`mm-harness launch ${adapter === "mobile" ? `${mobileTarget} ` : ""}--build`
|
|
132
172
|
);
|
|
133
173
|
}
|
|
134
|
-
|
|
174
|
+
stream.phase("install");
|
|
175
|
+
const ensured = await ensureOverlay(adapter, target, heal, state, machine);
|
|
135
176
|
if (!ensured.ok) {
|
|
136
|
-
return launchFail(
|
|
177
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
137
178
|
code: "OVERLAY_INSTALL_FAILED",
|
|
138
179
|
message: ensured.error ?? "overlay install failed",
|
|
139
180
|
recoverable: false,
|
|
140
181
|
exitCode: EXIT.infra
|
|
141
182
|
});
|
|
142
183
|
}
|
|
184
|
+
stream.phase("healthcheck");
|
|
143
185
|
if (adapter === "extension") {
|
|
144
186
|
const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
|
|
145
187
|
if (willBuild) {
|
|
146
188
|
const depsBlock = extensionDepsBlock(target);
|
|
147
189
|
if (depsBlock) {
|
|
148
190
|
if (heal === "off") {
|
|
149
|
-
return
|
|
191
|
+
return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
|
|
150
192
|
}
|
|
151
193
|
const recoveryCode2 = "deps.installed";
|
|
152
194
|
state.attemptedRecoveries.push(recoveryCode2);
|
|
195
|
+
stream.phase("recover");
|
|
153
196
|
const repaired = await installExtensionDeps(target);
|
|
154
197
|
if (repaired.status !== 0 || extensionDepsBlock(target)) {
|
|
155
|
-
return launchFail(
|
|
198
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
156
199
|
code: "DEPENDENCY_INSTALL_FAILED",
|
|
157
200
|
message: depsBlock.message,
|
|
158
201
|
recoverable: false,
|
|
@@ -166,12 +209,13 @@ async function handleLaunchLocked(argv) {
|
|
|
166
209
|
}
|
|
167
210
|
}
|
|
168
211
|
}
|
|
169
|
-
|
|
212
|
+
stream.phase("launch");
|
|
213
|
+
let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
|
|
170
214
|
if (attempt.status === 0) {
|
|
171
|
-
return await finishLaunch(
|
|
215
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
172
216
|
}
|
|
173
217
|
if (heal === "off") {
|
|
174
|
-
return launchFail(
|
|
218
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
175
219
|
code: "LAUNCH_FAILED",
|
|
176
220
|
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed (healing off)`,
|
|
177
221
|
recoverable: false,
|
|
@@ -181,12 +225,13 @@ async function handleLaunchLocked(argv) {
|
|
|
181
225
|
if (adapter === "extension" && tier === "quick" && extensionQuickReattachFailed(attempt.output)) {
|
|
182
226
|
const recoveryCode2 = "chrome.relaunched";
|
|
183
227
|
state.attemptedRecoveries.push(recoveryCode2);
|
|
184
|
-
|
|
228
|
+
stream.phase("recover");
|
|
229
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
185
230
|
if (rebuildAttempt.status === 0) {
|
|
186
231
|
state.recovered.push(recoveryCode2);
|
|
187
|
-
return await finishLaunch(
|
|
232
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
188
233
|
}
|
|
189
|
-
return launchFail(
|
|
234
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
190
235
|
code: "EXTENSION_RELAUNCH_FAILED",
|
|
191
236
|
message: "quick reattach failed, and clean relaunch failed too.",
|
|
192
237
|
recoverable: false,
|
|
@@ -195,7 +240,7 @@ async function handleLaunchLocked(argv) {
|
|
|
195
240
|
});
|
|
196
241
|
}
|
|
197
242
|
if (adapter === "mobile" && mobileProvisioningBlocked(attempt.output)) {
|
|
198
|
-
return launchFail(
|
|
243
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
199
244
|
code: "MOBILE_PROVISION_REQUIRED",
|
|
200
245
|
message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
|
|
201
246
|
recoverable: false,
|
|
@@ -206,7 +251,7 @@ async function handleLaunchLocked(argv) {
|
|
|
206
251
|
}
|
|
207
252
|
const bound = checkHealBounds(target, attempt.output, state);
|
|
208
253
|
if (bound !== null) {
|
|
209
|
-
return launchFail(
|
|
254
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
210
255
|
code: bound.code,
|
|
211
256
|
message: bound.message,
|
|
212
257
|
recoverable: false,
|
|
@@ -217,12 +262,13 @@ async function handleLaunchLocked(argv) {
|
|
|
217
262
|
}
|
|
218
263
|
const recoveryCode = RECOVERY_CODE[adapter];
|
|
219
264
|
state.attemptedRecoveries.push(recoveryCode);
|
|
220
|
-
|
|
265
|
+
stream.phase("recover");
|
|
266
|
+
attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
|
|
221
267
|
if (attempt.status === 0) {
|
|
222
268
|
state.recovered.push(recoveryCode);
|
|
223
|
-
return await finishLaunch(
|
|
269
|
+
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
224
270
|
}
|
|
225
|
-
return launchFail(
|
|
271
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
226
272
|
code: "SAME_RECOVERY_TWICE",
|
|
227
273
|
message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
|
|
228
274
|
recoverable: false,
|
|
@@ -275,14 +321,16 @@ function applyLaunchEnvOverrides(options, adapter, mobileTarget, target) {
|
|
|
275
321
|
return {
|
|
276
322
|
ok: false,
|
|
277
323
|
code: "DEVICE_WRONG_PLATFORM",
|
|
278
|
-
message: `--device ${device} did not resolve to an Android device for launch android
|
|
324
|
+
message: `--device ${device} did not resolve to an Android device for launch android.`,
|
|
325
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
279
326
|
};
|
|
280
327
|
}
|
|
281
328
|
if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
|
|
282
329
|
return {
|
|
283
330
|
ok: false,
|
|
284
331
|
code: "DEVICE_WRONG_PLATFORM",
|
|
285
|
-
message: `--device ${device} did not resolve to an iOS simulator for launch ios
|
|
332
|
+
message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
|
|
333
|
+
userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
|
|
286
334
|
};
|
|
287
335
|
}
|
|
288
336
|
}
|
|
@@ -328,26 +376,29 @@ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target
|
|
|
328
376
|
}
|
|
329
377
|
return launchExtension(target, tier, wantWatch, displayMode);
|
|
330
378
|
}
|
|
331
|
-
async function finishLaunch(
|
|
379
|
+
async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true) {
|
|
332
380
|
if (adapter === "extension" && displayMode === "sidepanel") {
|
|
333
381
|
const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
|
|
334
382
|
const sidepanelArgs = ["open"];
|
|
335
383
|
if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
336
|
-
if (!
|
|
384
|
+
if (!machine) {
|
|
337
385
|
console.error(
|
|
338
386
|
`[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
|
|
339
387
|
);
|
|
340
388
|
}
|
|
341
|
-
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target,
|
|
389
|
+
const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
|
|
342
390
|
if (sidepanel.status !== 0) {
|
|
343
391
|
const recoveryCode = "chrome.relaunched";
|
|
344
392
|
if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
|
|
345
393
|
state.attemptedRecoveries.push(recoveryCode);
|
|
346
|
-
|
|
394
|
+
stream.phase("recover");
|
|
395
|
+
const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
|
|
347
396
|
if (rebuildAttempt.status === 0) {
|
|
348
397
|
state.recovered.push(recoveryCode);
|
|
349
398
|
return await finishLaunch(
|
|
350
|
-
|
|
399
|
+
jsonOutput,
|
|
400
|
+
machine,
|
|
401
|
+
stream,
|
|
351
402
|
adapter,
|
|
352
403
|
mobileTarget,
|
|
353
404
|
tier,
|
|
@@ -359,7 +410,7 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
359
410
|
false
|
|
360
411
|
);
|
|
361
412
|
}
|
|
362
|
-
return launchFail(
|
|
413
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
363
414
|
code: "EXTENSION_RELAUNCH_FAILED",
|
|
364
415
|
message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
|
|
365
416
|
recoverable: false,
|
|
@@ -367,7 +418,7 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
367
418
|
originalError: rebuildAttempt.output.trim() || void 0
|
|
368
419
|
});
|
|
369
420
|
}
|
|
370
|
-
return launchFail(
|
|
421
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
371
422
|
code: "SIDEPANEL_OPEN_FAILED",
|
|
372
423
|
message: "app launched but opening the side panel failed.",
|
|
373
424
|
recoverable: false,
|
|
@@ -377,16 +428,48 @@ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, targ
|
|
|
377
428
|
});
|
|
378
429
|
}
|
|
379
430
|
}
|
|
431
|
+
if (adapter === "extension") {
|
|
432
|
+
try {
|
|
433
|
+
await ensureExtensionConsoleCapture(target);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!machine) {
|
|
436
|
+
console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
380
440
|
if (wantVerify) {
|
|
441
|
+
stream.phase("verify");
|
|
381
442
|
const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
|
|
382
443
|
if (adapter === "mobile" && mobileTarget) verifyArgs.push("--platform", mobileTarget);
|
|
383
|
-
if (
|
|
384
|
-
|
|
444
|
+
if (machine) verifyArgs.push("--json");
|
|
445
|
+
const exitCode = await handleHarness(verifyArgs);
|
|
446
|
+
if (stream.enabled) {
|
|
447
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
448
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
449
|
+
stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
|
|
450
|
+
adapter,
|
|
451
|
+
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
452
|
+
tier,
|
|
453
|
+
recovered: state.recovered,
|
|
454
|
+
mutations: state.mutations
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return exitCode;
|
|
385
458
|
}
|
|
386
|
-
return launchPass(
|
|
459
|
+
return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
|
|
387
460
|
}
|
|
388
|
-
function launchPass(json, adapter, mobileTarget, tier, displayMode, state) {
|
|
389
|
-
if (
|
|
461
|
+
function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
|
|
462
|
+
if (stream.enabled) {
|
|
463
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
464
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
465
|
+
stream.complete("pass", EXIT.ok, {
|
|
466
|
+
adapter,
|
|
467
|
+
platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
|
|
468
|
+
tier,
|
|
469
|
+
recovered: state.recovered,
|
|
470
|
+
mutations: state.mutations
|
|
471
|
+
});
|
|
472
|
+
} else if (json) {
|
|
390
473
|
console.log(
|
|
391
474
|
JSON.stringify(
|
|
392
475
|
{
|
|
@@ -424,8 +507,32 @@ function launchDeviceLabel(mobileTarget) {
|
|
|
424
507
|
}
|
|
425
508
|
return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
|
|
426
509
|
}
|
|
427
|
-
function
|
|
428
|
-
|
|
510
|
+
function launchUsage(json, stream, message, userAction) {
|
|
511
|
+
stream.error({ code: "USAGE", message, userAction });
|
|
512
|
+
return usageOut(json, "launch", message, userAction);
|
|
513
|
+
}
|
|
514
|
+
function launchFail(json, stream, adapter, mobileTarget, tier, state, target, failure) {
|
|
515
|
+
const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
|
|
516
|
+
if (stream.enabled) {
|
|
517
|
+
for (const mutation of state.mutations) stream.mutation(mutation);
|
|
518
|
+
for (const recovery of state.recovered) stream.recovery(recovery);
|
|
519
|
+
stream.error({
|
|
520
|
+
code: failure.code,
|
|
521
|
+
message: failure.message,
|
|
522
|
+
retryable: failure.recoverable,
|
|
523
|
+
userAction,
|
|
524
|
+
originalError: failure.originalError ?? null
|
|
525
|
+
});
|
|
526
|
+
stream.complete("fail", failure.exitCode, {
|
|
527
|
+
adapter,
|
|
528
|
+
platform: mobileTarget ?? null,
|
|
529
|
+
tier,
|
|
530
|
+
recovered: state.recovered,
|
|
531
|
+
mutations: state.mutations,
|
|
532
|
+
recoverable: failure.recoverable,
|
|
533
|
+
attemptedRecoveries: state.attemptedRecoveries
|
|
534
|
+
});
|
|
535
|
+
} else if (json) {
|
|
429
536
|
console.log(
|
|
430
537
|
JSON.stringify(
|
|
431
538
|
{
|
|
@@ -445,7 +552,7 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
|
445
552
|
code: failure.code,
|
|
446
553
|
message: failure.message,
|
|
447
554
|
retryable: failure.recoverable,
|
|
448
|
-
userAction
|
|
555
|
+
userAction,
|
|
449
556
|
originalError: failure.originalError ?? null
|
|
450
557
|
}
|
|
451
558
|
},
|
|
@@ -458,8 +565,8 @@ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
|
|
|
458
565
|
`\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
|
|
459
566
|
${failure.message}` + (failure.originalError ? `
|
|
460
567
|
--- original failure ---
|
|
461
|
-
${failure.originalError}` : "") +
|
|
462
|
-
Next: ${
|
|
568
|
+
${failure.originalError}` : "") + `
|
|
569
|
+
Next: ${userAction}`
|
|
463
570
|
);
|
|
464
571
|
}
|
|
465
572
|
return failure.exitCode;
|