@deeeed/metamask-harness 0.72.0 → 0.74.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.
@@ -1,9 +1,4 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import fs from "node:fs";
3
- import path from "node:path";
4
- import { color, colorHumanMessage } from "../../cli-color.js";
5
- import { handleHarness } from "../../harness.js";
6
- import { recipeRuntimePath, runnerDir, walletFixturePath } from "../../paths.js";
7
2
  import { getAdapterSurface } from "../../adapters/surface.js";
8
3
  import {
9
4
  ADAPTER_DETECT_NEXT,
@@ -12,44 +7,14 @@ import {
12
7
  flag,
13
8
  parseFlags,
14
9
  resolveAdapter,
15
- spawnScript,
16
10
  str,
17
11
  targetOf,
18
- usageOut,
19
- writeInteractiveProgress
12
+ usageOut
20
13
  } from "../shared.js";
21
- import {
22
- RECOVERY_CODE,
23
- checkHealBounds,
24
- ensureOverlay,
25
- newHealState,
26
- parseHeal
27
- } from "../../heal-bounds.js";
28
- import {
29
- extensionDepsBlock,
30
- extensionRuntimeReusable,
31
- installExtensionDeps,
32
- launchExtension
33
- } from "./extension.js";
34
- import { launchMobile } from "./mobile.js";
35
- import { applyDeviceTargeting } from "../device-target.js";
14
+ import { parseHeal } from "../../heal-bounds.js";
36
15
  import { acquireCheckoutLock } from "../../checkout-lock.js";
37
16
  import { JsonStreamWriter } from "../../json-stream.js";
38
- import {
39
- FeatureFlagSpecError,
40
- fixtureFeatureFlags,
41
- parseRemoteFlagSpec,
42
- remoteFlagRequestFor,
43
- sameFeatureFlags,
44
- toRemoteFlagSpec
45
- } from "../../feature-flags.js";
46
- import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
47
- import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
48
- import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
49
- import {
50
- releaseArtifactState,
51
- runtimeDistCheck
52
- } from "../../adapters/extension/runtime-decision.js";
17
+ import { shellQuote } from "./device.js";
53
18
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
54
19
  "build",
55
20
  "watch",
@@ -64,22 +29,6 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
64
29
  "headless",
65
30
  "headful"
66
31
  ]);
67
- const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
68
- function metroLaunchEvidence(adapter, target) {
69
- if (adapter !== "mobile") return void 0;
70
- const evidencePath = recipeRuntimePath(target, "metro-generation.json");
71
- let descriptor;
72
- try {
73
- descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
74
- const stat = fs.fstatSync(descriptor);
75
- if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
76
- return JSON.parse(fs.readFileSync(descriptor, "utf8"));
77
- } catch {
78
- return void 0;
79
- } finally {
80
- if (descriptor !== void 0) fs.closeSync(descriptor);
81
- }
82
- }
83
32
  async function handleLaunch(argv) {
84
33
  const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
85
34
  const json = flag(options, "json");
@@ -120,83 +69,6 @@ async function handleLaunch(argv) {
120
69
  restoreStdout();
121
70
  }
122
71
  }
123
- const TERMINAL_ONLY_LAUNCH_OPTIONS = Object.freeze({
124
- signer: "--signer",
125
- account: "--account",
126
- freshProfile: "--fresh-profile",
127
- headless: "--headless",
128
- headful: "--headful",
129
- slowMo: "--slow-mo",
130
- network: "--network",
131
- mainnetConfirmation: "--mainnet-confirmation"
132
- });
133
- function terminalOnlyLaunchFlags(options) {
134
- return Object.entries(TERMINAL_ONLY_LAUNCH_OPTIONS).filter(([key]) => options[key] !== void 0 && options[key] !== false).map(([, name]) => name);
135
- }
136
- function parseTerminalLauncherOutput(output) {
137
- const text = output.trim();
138
- if (!text) return { status: "fail", error: "the terminal launcher printed no summary" };
139
- try {
140
- return JSON.parse(text);
141
- } catch {
142
- const last = text.split("\n").filter((line) => line.trim()).pop() ?? "";
143
- try {
144
- return JSON.parse(last);
145
- } catch {
146
- return { status: "fail", error: `the terminal launcher printed no JSON summary: ${last.slice(0, 200)}` };
147
- }
148
- }
149
- }
150
- function launchTerminal(options, target, jsonOutput, stream) {
151
- const cdpPort = str(options, "cdpPort") ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
152
- const appPort = str(options, "watcherPort") ?? process.env.TERMINAL_APP_PORT ?? process.env.RECIPE_WATCHER_PORT ?? process.env.WATCHER_PORT;
153
- if (!cdpPort || !appPort) {
154
- return launchUsage(
155
- jsonOutput,
156
- stream,
157
- "terminal launch needs the slot browser CDP port and the slot dev-server port.",
158
- "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>"
159
- );
160
- }
161
- const args = [
162
- path.join(runnerDir, "adapters", "terminal", "launch.mjs"),
163
- "--target",
164
- target,
165
- "--cdp-port",
166
- cdpPort,
167
- "--app-port",
168
- appPort,
169
- "--signer",
170
- str(options, "signer") ?? process.env.TERMINAL_SIGNER ?? "extension",
171
- "--account",
172
- str(options, "account") ?? process.env.TERMINAL_ACCOUNT ?? "dev1",
173
- "--json"
174
- ];
175
- if (flag(options, "freshProfile")) args.push("--fresh-profile");
176
- if (flag(options, "headless")) args.push("--headless");
177
- if (flag(options, "headful")) args.push("--headful");
178
- const slowMo = str(options, "slowMo");
179
- if (slowMo) args.push("--slow-mo", slowMo);
180
- const network = str(options, "network");
181
- if (network) args.push("--network", network);
182
- const mainnetConfirmation = str(options, "mainnetConfirmation");
183
- if (mainnetConfirmation) args.push("--mainnet-confirmation", mainnetConfirmation);
184
- stream.phase("launch", { adapter: "terminal", cdpPort, appPort });
185
- let output = "";
186
- let status = 0;
187
- try {
188
- output = execFileSync(process.execPath, args, { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
189
- } catch (error) {
190
- status = 1;
191
- output = String(error.stdout ?? "");
192
- }
193
- const result = parseTerminalLauncherOutput(output);
194
- if (jsonOutput) console.log(JSON.stringify({ schemaVersion: 1, command: "launch", adapter: "terminal", target, ...result }, null, 2));
195
- else if (result.status === "pass") console.log(`\u2713 terminal browser ${result.reused ? "reused" : "started"} on CDP ${result.cdpPort} (${result.signer}, ${result.account?.name})`);
196
- else console.error(`\u2717 terminal launch failed: ${result.error ?? "unknown error"}`);
197
- stream.complete(result.status === "pass" ? "pass" : "fail", status);
198
- return result.status === "pass" ? 0 : 1;
199
- }
200
72
  async function handleLaunchLocked(argv, stream) {
201
73
  const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
202
74
  const json = flag(options, "json");
@@ -216,827 +88,22 @@ async function handleLaunchLocked(argv, stream) {
216
88
  return launchUsage(jsonOutput, stream, `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
217
89
  }
218
90
  stream.phase("resolve", { target, adapter, platform: mobileTarget ?? null });
219
- if (adapter === "core") {
220
- return launchUsage(jsonOutput, stream, "core is headless; there is nothing to launch.", "mm-harness verify");
221
- }
222
- if (adapter === "terminal") {
223
- return launchTerminal(options, target, jsonOutput, stream);
224
- }
225
- const terminalOnly = terminalOnlyLaunchFlags(options);
226
- if (terminalOnly.length > 0) {
227
- return launchUsage(
228
- jsonOutput,
229
- stream,
230
- `${terminalOnly.join(", ")} apply only to --adapter terminal; ${adapter} does not read them.`,
231
- "drop the terminal flags, or pass --adapter terminal"
232
- );
233
- }
234
- if (adapter === "mobile" && !mobileTarget) {
235
- return launchUsage(
236
- jsonOutput,
237
- stream,
238
- "target is required for mobile.",
239
- "mm-harness launch ios or mm-harness launch android"
240
- );
241
- }
242
- const wantBuild = flag(options, "build");
243
- const wantVerify = flag(options, "verify");
244
- const wantWatch = flag(options, "watch");
245
- const wantRunway = flag(options, "runway");
246
- const wantClearMetro = flag(options, "clearMetro");
247
- const requestedSurface = str(options, "surface");
248
- const legacySidepanel = flag(options, "sidepanel");
249
- const legacyFullscreen = flag(options, "fullscreen");
250
- if (wantRunway && adapter !== "mobile") {
251
- return launchUsage(jsonOutput, stream, "runway is mobile-only.", "drop --runway for the extension");
252
- }
253
- if (wantClearMetro && adapter !== "mobile") {
254
- return launchUsage(jsonOutput, stream, "--clear-metro is mobile-only.", "drop --clear-metro for the extension");
255
- }
256
- if (requestedSurface !== void 0 && requestedSurface !== "fullscreen" && requestedSurface !== "sidepanel") {
257
- return launchUsage(
258
- jsonOutput,
259
- stream,
260
- `--surface must be fullscreen or sidepanel (got: ${requestedSurface}).`,
261
- "pass --surface fullscreen or --surface sidepanel"
262
- );
263
- }
264
- if (legacySidepanel && legacyFullscreen) {
265
- return launchUsage(
266
- jsonOutput,
267
- stream,
268
- "--sidepanel and --fullscreen conflict.",
269
- "pass exactly one Extension surface"
270
- );
271
- }
272
- const legacySurface = legacySidepanel ? "sidepanel" : legacyFullscreen ? "fullscreen" : void 0;
273
- if (requestedSurface && legacySurface && requestedSurface !== legacySurface) {
274
- return launchUsage(
275
- jsonOutput,
276
- stream,
277
- `--surface ${requestedSurface} conflicts with --${legacySurface}.`,
278
- "pass one consistent Extension surface"
279
- );
280
- }
281
- const displayMode = requestedSurface ?? legacySurface ?? "fullscreen";
282
- const remoteFlagSpec = str(options, "remoteFlag");
283
- if (remoteFlagSpec !== void 0 && adapter === "extension" && wantWatch) {
284
- return launchUsage(
285
- jsonOutput,
286
- stream,
287
- "--remote-flag cannot be applied with --watch: the watcher never re-creates the dist snapshot that carries the pin.",
288
- `mm-harness launch --adapter extension --remote-flag ${remoteFlagSpec} --target ${shellQuote(target)}`
289
- );
290
- }
291
- let remoteFlagRequest;
292
- try {
293
- remoteFlagRequest = remoteFlagRequestFor({
294
- ...fixtureFeatureFlags(walletFixturePath(target)),
295
- ...remoteFlagSpec === void 0 ? {} : parseRemoteFlagSpec(remoteFlagSpec)
296
- });
297
- } catch (error) {
298
- if (!(error instanceof FeatureFlagSpecError)) throw error;
299
- return launchUsage(jsonOutput, stream, error.message, error.userAction);
300
- }
301
- for (const portFlag of ["cdpPort", "watcherPort"]) {
302
- const value = str(options, portFlag);
303
- if (value !== void 0 && !/^\d+$/u.test(value)) {
304
- return launchUsage(
305
- jsonOutput,
306
- stream,
307
- `--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
308
- `pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
309
- );
310
- }
311
- }
312
- const envResult = applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
313
- if (envResult && "code" in envResult) {
314
- return launchUsage(jsonOutput, stream, envResult.message, envResult.userAction);
315
- }
316
- const tier = wantBuild ? "build" : "quick";
317
- if (adapter === "extension") {
318
- const requestedUrl = typeof options["url"] === "string" && options["url"] ? options["url"] : "";
319
- if (requestedUrl || displayMode === "sidepanel") {
320
- process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
321
- }
322
- }
323
- if (!machine && adapter === "extension") {
324
- const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
325
- const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
326
- console.error(colorHumanMessage(
327
- `\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
328
- ));
329
- }
330
- if (adapter === "mobile") {
331
- 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";
332
- const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
333
- writeInteractiveProgress(
334
- machine,
335
- `\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
336
- );
337
- }
338
- const state = newHealState();
339
- if (tier === "quick" && nativeInputsChanged(target, adapter)) {
340
- return launchUsage(
341
- jsonOutput,
342
- stream,
343
- "native build inputs changed since last build.",
344
- `mm-harness launch ${adapter === "mobile" ? `${mobileTarget} ` : ""}--build`
345
- );
346
- }
347
- stream.phase("install");
348
- const ensured = await ensureOverlay(adapter, target, heal, state, machine);
349
- if (!ensured.ok) {
350
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
351
- code: "OVERLAY_INSTALL_FAILED",
352
- message: ensured.error ?? "overlay install failed",
353
- recoverable: false,
354
- exitCode: EXIT.infra
355
- });
356
- }
357
- stream.phase("healthcheck");
358
- if (adapter === "extension") {
359
- const artifact = releaseArtifactState(target);
360
- const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
361
- const verifiedReleaseReuse = artifact.status === "valid" && reusable;
362
- const verifiedReleaseLaunch = !wantWatch && tier !== "build" && artifact.status === "valid" && runtimeDistCheck(target, artifact).status === "fresh";
363
- const willBuild = !reusable;
364
- if (willBuild) {
365
- const productConfigBlock = extensionProductConfigBlock(target);
366
- if (productConfigBlock) {
367
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
368
- code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
369
- message: productConfigBlock.message,
370
- recoverable: false,
371
- userAction: productConfigBlock.userAction,
372
- exitCode: EXIT.infra
373
- });
374
- }
375
- }
376
- const depsBlock = verifiedReleaseReuse || verifiedReleaseLaunch ? null : extensionDepsBlock(target);
377
- if (depsBlock) {
378
- if (heal === "off") {
379
- return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
380
- }
381
- const recoveryCode2 = "deps.installed";
382
- state.attemptedRecoveries.push(recoveryCode2);
383
- stream.phase("recover");
384
- const repaired = await installExtensionDeps(target);
385
- if (repaired.status !== 0 || extensionDepsBlock(target)) {
386
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
387
- code: "DEPENDENCY_INSTALL_FAILED",
388
- message: depsBlock.message,
389
- recoverable: false,
390
- userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
391
- originalError: repaired.output,
392
- exitCode: EXIT.infra
393
- });
394
- }
395
- state.recovered.push(recoveryCode2);
396
- state.mutations.push({ type: "dependencies", action: "installed", path: target });
397
- }
398
- }
399
- stream.phase("launch");
400
- let attempt = await executeComposition(
91
+ return getAdapterSurface(adapter).launch({
401
92
  adapter,
402
- mobileTarget,
403
- tier,
404
- wantWatch,
405
93
  target,
406
- machine,
407
- displayMode,
408
- false,
409
- wantClearMetro,
410
- remoteFlagRequest
411
- );
412
- if (attempt.status === 0) {
413
- return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
414
- }
415
- const readinessFailure = mobileReadinessFailure(attempt);
416
- if (readinessFailure) {
417
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, readinessFailure);
418
- }
419
- if (adapter === "extension" && attempt.output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
420
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
421
- code: "REMOTE_FLAG_NOT_PINNABLE",
422
- message: `a verified release artifact cannot carry a remote feature-flag pin (${remoteFlagRequest?.spec ?? ""}); its manifest is signed evidence and is never rewritten. Validate the artifact as shipped (drop --remote-flag and the fixture's remoteFeatureFlags) or validate the variant on a source build.`,
423
- recoverable: false,
424
- // The next step must leave the artifact alone: a --build here would replace
425
- // the artifact under validation with a source build.
426
- userAction: Object.keys(fixtureFeatureFlags(walletFixturePath(target))).length > 0 ? `remove remoteFeatureFlags from ${shellQuote(walletFixturePath(target))}, then: mm-harness launch --adapter extension --target ${shellQuote(target)}` : `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
427
- exitCode: EXIT.infra,
428
- originalError: attempt.output.trim() || void 0
429
- });
430
- }
431
- if (adapter === "extension" && extensionProductConfigMissing(attempt.output)) {
432
- const block = extensionProductConfigBlock(target);
433
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
434
- code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
435
- message: block?.message ?? "Extension product configuration was not ready when the build started.",
436
- recoverable: false,
437
- userAction: block?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
438
- exitCode: EXIT.infra
439
- });
440
- }
441
- if (adapter === "extension" && extensionBrowserMissing(attempt.output)) {
442
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
443
- code: "EXTENSION_BROWSER_REQUIRED",
444
- message: "the checkout Playwright browser is not installed; system Chrome is not a compatible unpacked-extension fallback. Installing the browser cache requires user approval.",
445
- recoverable: false,
446
- userAction: `ask the user for approval, then run: cd ${shellQuote(target)} && yarn playwright install chromium`,
447
- exitCode: EXIT.infra
448
- });
449
- }
450
- if (adapter === "extension" && (attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || attempt.output.includes("EXTENSION_STARTUP_UNVERIFIED:"))) {
451
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
452
- code: attempt.output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
453
- message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
454
- recoverable: false,
455
- userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
456
- exitCode: EXIT.runtime,
457
- originalError: attempt.output.trim() || void 0
458
- });
459
- }
460
- if (adapter === "extension" && attempt.output.includes("EVM_RPC_EXECUTION_FAILED:")) {
461
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
462
- code: "EVM_RPC_EXECUTION_FAILED",
463
- message: "Background RPC fetch failed during JavaScript execution; this does not establish an Infura authentication or connectivity failure.",
464
- recoverable: false,
465
- userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
466
- exitCode: EXIT.runtime,
467
- originalError: attempt.output.trim() || void 0
468
- });
469
- }
470
- if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
471
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
472
- code: "EVM_RPC_UNREACHABLE",
473
- message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
474
- recoverable: false,
475
- userAction: `Check the effective INFURA_PROJECT_ID from the environment, .metamaskprodrc, or ${shellQuote(path.join(target, ".metamaskrc"))}, then rerun: mm-harness launch --verify --target ${shellQuote(target)}`,
476
- exitCode: EXIT.runtime,
477
- originalError: attempt.output.trim() || void 0
478
- });
479
- }
480
- if (adapter === "mobile" && attempt.output.includes("open-device: Android device is locked.")) {
481
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
482
- code: "MOBILE_DEVICE_LOCKED",
483
- message: "the selected Android device is locked.",
484
- recoverable: false,
485
- userAction: "Unlock the selected Android device, then retry the launch.",
486
- exitCode: EXIT.runtime,
487
- originalError: attempt.output.trim()
488
- });
489
- }
490
- if (adapter === "mobile" && /open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(attempt.output)) {
491
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
492
- code: "MOBILE_DEVICE_UI_UNAVAILABLE",
493
- message: "the selected Android display could not be prepared.",
494
- recoverable: false,
495
- userAction: "Check adb access and the selected device screen, then retry the launch.",
496
- exitCode: EXIT.runtime,
497
- originalError: attempt.output.trim()
498
- });
499
- }
500
- if (heal === "off") {
501
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
502
- code: "LAUNCH_FAILED",
503
- message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed (healing off)`,
504
- recoverable: false,
505
- exitCode: EXIT.infra
506
- });
507
- }
508
- if (adapter === "extension" && tier === "quick" && extensionQuickReattachFailed(attempt.output)) {
509
- const recoveryCode2 = "chrome.relaunched";
510
- state.attemptedRecoveries.push(recoveryCode2);
511
- stream.phase("recover");
512
- const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode, false, false, remoteFlagRequest);
513
- if (rebuildAttempt.status === 0) {
514
- state.recovered.push(recoveryCode2);
515
- return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, rebuildAttempt.remoteFeatureFlags);
516
- }
517
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
518
- code: "EXTENSION_RELAUNCH_FAILED",
519
- message: "quick reattach failed, and clean relaunch failed too.",
520
- recoverable: false,
521
- exitCode: EXIT.infra,
522
- originalError: rebuildAttempt.output.trim() || void 0
523
- });
524
- }
525
- const missingIosSimulator = adapter === "mobile" ? missingConfiguredIosSimulator(attempt.output) : void 0;
526
- const ambiguousIosSimulator = adapter === "mobile" ? ambiguousConfiguredIosSimulator(attempt.output) : void 0;
527
- if (ambiguousIosSimulator) {
528
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
529
- code: "MOBILE_DEVICE_AMBIGUOUS",
530
- message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
531
- recoverable: false,
532
- userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
533
- exitCode: EXIT.runtime,
534
- originalError: attempt.output.trim() || void 0
535
- });
536
- }
537
- if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
538
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
539
- code: "MOBILE_DEVICE_NOT_FOUND",
540
- message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
541
- recoverable: false,
542
- userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
543
- exitCode: EXIT.runtime,
544
- originalError: attempt.output.trim() || void 0
545
- });
546
- }
547
- if (adapter === "mobile" && mobileProvisioningBlocked(attempt.output)) {
548
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
549
- code: "MOBILE_PROVISION_REQUIRED",
550
- message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
551
- recoverable: false,
552
- userAction: mobileProvisionCommand(target, mobileTarget),
553
- exitCode: EXIT.runtime,
554
- originalError: attempt.output.trim() || void 0
555
- });
556
- }
557
- if (adapter === "mobile" && tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(attempt.output)) {
558
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
559
- code: "MOBILE_NATIVE_BUILD_FAILED",
560
- message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
561
- recoverable: false,
562
- exitCode: EXIT.runtime,
563
- originalError: attempt.output.trim() || void 0
564
- });
565
- }
566
- const bound = checkHealBounds(target, attempt.output, state);
567
- if (bound !== null) {
568
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
569
- code: bound.code,
570
- message: bound.message,
571
- recoverable: false,
572
- userAction: bound.userAction,
573
- exitCode: bound.exitCode,
574
- originalError: bound.originalError
575
- });
576
- }
577
- const restartMobileApp = adapter === "mobile" && mobileBridgeTargetMissing(attempt.output);
578
- const recoveryCode = restartMobileApp ? "mobile.app-restarted" : RECOVERY_CODE[adapter];
579
- state.attemptedRecoveries.push(recoveryCode);
580
- stream.phase("recover");
581
- const recoveryTier = restartMobileApp && tier === "build" ? "quick" : tier;
582
- if (!jsonOutput) {
583
- process.stderr.write(`launch: recovering (${recoveryCode}) with the ${recoveryTier} tier
584
- `);
585
- }
586
- attempt = await executeComposition(
587
- adapter,
94
+ options,
588
95
  mobileTarget,
589
- recoveryTier,
590
- wantWatch,
591
- target,
592
- machine,
593
- displayMode,
594
- restartMobileApp,
595
- false,
596
- remoteFlagRequest
597
- );
598
- if (attempt.status === 0) {
599
- state.recovered.push(recoveryCode);
600
- return await settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, true, attempt.remoteFeatureFlags);
601
- }
602
- const retryReadinessFailure = mobileReadinessFailure(attempt);
603
- if (retryReadinessFailure) {
604
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, retryReadinessFailure);
605
- }
606
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
607
- code: "SAME_RECOVERY_TWICE",
608
- message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
609
- recoverable: false,
610
- exitCode: EXIT.bounded,
611
- originalError: attempt.output.trim() || void 0
612
- });
613
- }
614
- function mobileReadinessFailure(result) {
615
- const block = result.readinessBlock;
616
- if (!block) return void 0;
617
- return {
618
- code: block.reasonCode === "deps-not-ready" ? "MOBILE_DEPENDENCIES_NOT_READY" : "MOBILE_READINESS_BLOCKED",
619
- message: block.message,
620
- userAction: block.userAction,
621
- recoverable: false,
622
- exitCode: result.status
623
- };
624
- }
625
- function extensionQuickReattachFailed(output) {
626
- return /\[reattach\]|extension quick reattach|mm-harness launch --build/u.test(output);
627
- }
628
- function extensionRuntimeBlocked(output) {
629
- return /ERR_BLOCKED_BY_CLIENT|has been blocked by Chromium/u.test(output);
630
- }
631
- function extensionBrowserMissing(output) {
632
- return /Playwright Chromium is not installed|No compatible Chromium binary selected/u.test(output);
633
- }
634
- function extensionProductConfigMissing(output) {
635
- return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
636
- }
637
- function extensionEvmRpcUnreachable(output) {
638
- return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
639
- }
640
- function mobileProvisioningBlocked(output) {
641
- 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);
642
- }
643
- function missingConfiguredIosSimulator(output) {
644
- return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
645
- }
646
- function ambiguousConfiguredIosSimulator(output) {
647
- return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
648
- }
649
- function mobileBridgeTargetMissing(output) {
650
- return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
651
- }
652
- function mobileProvisionCommand(target, mobileTarget) {
653
- const platform = mobileTarget === "android" ? "android" : "ios";
654
- const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
655
- const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
656
- if (device) parts.push("--device", shellQuote(device));
657
- return parts.join(" ");
658
- }
659
- function shellQuote(value) {
660
- if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
661
- return `'${value.replace(/'/gu, `'"'"'`)}'`;
662
- }
663
- function applyLaunchEnvOverrides(options, adapter, mobileTarget, target) {
664
- getAdapterSurface(adapter).resolveSlotPorts(target);
665
- const device = str(options, "device");
666
- if (adapter === "mobile" && (device || mobileTarget === "ios" || mobileTarget === "android")) {
667
- const targetingOptions = {
668
- ...options,
669
- ...mobileTarget ? { platform: mobileTarget } : {}
670
- };
671
- const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
672
- if ("code" in result) {
673
- if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
674
- process.env.IOS_SIMULATOR = device;
675
- process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
676
- process.env.ADB_SERIAL = "";
677
- process.env.ANDROID_SERIAL = "";
678
- process.env.ANDROID_DEVICE = "";
679
- process.env.ANDROID_TARGET_DEVICE_NAME = "";
680
- } else {
681
- return result;
682
- }
683
- }
684
- if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
685
- return {
686
- ok: false,
687
- code: "DEVICE_WRONG_PLATFORM",
688
- message: `--device ${device} did not resolve to an Android device for launch android.`,
689
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
690
- };
691
- }
692
- if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
693
- return {
694
- ok: false,
695
- code: "DEVICE_WRONG_PLATFORM",
696
- message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
697
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
698
- };
699
- }
700
- }
701
- const cdpPort = str(options, "cdpPort");
702
- if (cdpPort) {
703
- process.env.CDP_PORT = cdpPort;
704
- process.env.RECIPE_CDP_PORT = cdpPort;
705
- }
706
- const watcherPort = str(options, "watcherPort");
707
- if (watcherPort) {
708
- process.env.WATCHER_PORT = watcherPort;
709
- process.env.RECIPE_WATCHER_PORT = watcherPort;
710
- process.env.METRO_PORT = watcherPort;
711
- }
712
- return { ok: true };
713
- }
714
- function isIosSimulatorUdid(value) {
715
- return /^[0-9A-Fa-f-]{36}$/u.test(value);
716
- }
717
- function nativeInputsChanged(target, adapter) {
718
- const baselineFile = recipeRuntimePath(target, ".last-build-ref");
719
- let baseline;
720
- try {
721
- baseline = fs.readFileSync(baselineFile, "utf8").trim();
722
- } catch {
723
- return false;
724
- }
725
- if (!baseline) return false;
726
- const paths = adapter === "mobile" ? ["ios", "android", "package.json", "yarn.lock"] : ["package.json", "yarn.lock", "webpack.config.js"];
727
- try {
728
- const diff = execFileSync("git", ["-C", target, "diff", "--name-only", baseline, "--", ...paths], {
729
- encoding: "utf8",
730
- stdio: ["ignore", "pipe", "ignore"]
731
- }).trim();
732
- return diff.length > 0;
733
- } catch {
734
- return false;
735
- }
736
- }
737
- function verifyExtensionPin(target, requested) {
738
- const pinned = readExtensionPinnedFlags(target);
739
- if (pinned.error) {
740
- return {
741
- ok: false,
742
- flags: {},
743
- code: "RUNTIME_MANIFEST_UNREADABLE",
744
- message: `the launched dist snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}`
745
- };
746
- }
747
- if (pinned.overrides !== null && pinned.pinned === null) {
748
- return {
749
- ok: false,
750
- flags: {},
751
- code: "REMOTE_FLAG_NOT_APPLIED",
752
- message: `the launched dist snapshot manifest (${pinned.sourcePath ?? "unknown path"}) carries no harness pin record; it was not written by this launch.`
753
- };
754
- }
755
- const actual = pinned.pinned ?? {};
756
- if (!sameFeatureFlags(actual, requested)) {
757
- const mismatched = [.../* @__PURE__ */ new Set([...Object.keys(requested), ...Object.keys(actual)])].filter(
758
- (key) => JSON.stringify(actual[key]) !== JSON.stringify(requested[key])
759
- );
760
- return {
761
- ok: false,
762
- flags: {},
763
- code: "REMOTE_FLAG_NOT_APPLIED",
764
- message: `the remote feature-flag pin ${toRemoteFlagSpec(requested) || "(none)"} does not match the launched snapshot manifest: ` + mismatched.map((key) => `${key} expected ${JSON.stringify(requested[key])}, got ${JSON.stringify(actual[key])}`).join("; ")
765
- };
766
- }
767
- return { ok: true, flags: actual, message: "" };
768
- }
769
- async function executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode = "fullscreen", restartMobileApp = false, clearMobileMetro = false, remoteFlagRequest) {
770
- if (adapter === "mobile") {
771
- return launchMobile(
772
- target,
773
- mobileTarget,
774
- tier,
775
- json,
776
- restartMobileApp,
777
- clearMobileMetro,
778
- remoteFlagRequest?.flags
779
- );
780
- }
781
- return launchExtension(target, tier, wantWatch, displayMode, remoteFlagRequest);
782
- }
783
- async function settleLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, remoteFlagRequest, sidepanelRecoveryAllowed = true, mobileProvenFlags) {
784
- const requested = remoteFlagRequest?.flags ?? {};
785
- const pinned = adapter !== "extension" ? { ok: true, flags: mobileProvenFlags === void 0 ? requested : mobileProvenFlags, message: "" } : wantWatch ? { ok: true, flags: readExtensionPinnedFlags(target).pinned, message: "" } : verifyExtensionPin(target, requested);
786
- if (!pinned.ok) {
787
- const spec = remoteFlagRequest?.spec;
788
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
789
- code: pinned.code ?? "REMOTE_FLAG_NOT_APPLIED",
790
- message: pinned.message,
791
- recoverable: false,
792
- // A rebuild re-creates the snapshot; the flag is repeated only when one was asked for.
793
- userAction: `mm-harness launch --adapter extension --build${spec ? ` --remote-flag ${spec}` : ""} --target ${shellQuote(target)}`,
794
- exitCode: EXIT.runtime
795
- });
796
- }
797
- return finishLaunch(
96
+ heal,
798
97
  jsonOutput,
799
98
  machine,
800
99
  stream,
801
- adapter,
802
- mobileTarget,
803
- tier,
804
- displayMode,
805
- target,
806
- state,
807
- wantWatch,
808
- wantVerify,
809
- sidepanelRecoveryAllowed,
810
- remoteFlagRequest,
811
- pinned.flags
812
- );
813
- }
814
- async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true, remoteFlagRequest, remoteFlags = {}) {
815
- if (adapter === "extension" && displayMode === "fullscreen") {
816
- const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
817
- const sidepanelArgs = ["close"];
818
- if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
819
- const fullscreen = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
820
- if (fullscreen.status !== 0) {
821
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
822
- code: "FULLSCREEN_OPEN_FAILED",
823
- message: "app launched but restoring the fullscreen Extension surface failed.",
824
- recoverable: false,
825
- userAction: `bash adapters/extension/sidepanel-toggle.sh close --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
826
- exitCode: EXIT.runtime,
827
- originalError: fullscreen.output.trim() || void 0
828
- });
829
- }
830
- }
831
- if (adapter === "extension" && displayMode === "sidepanel") {
832
- const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
833
- const sidepanelArgs = ["open"];
834
- if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
835
- if (!machine) {
836
- console.error(
837
- `[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
838
- );
839
- }
840
- const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
841
- if (sidepanel.status !== 0) {
842
- const recoveryCode = "chrome.relaunched";
843
- if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
844
- state.attemptedRecoveries.push(recoveryCode);
845
- stream.phase("recover");
846
- const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode, false, false, remoteFlagRequest);
847
- if (rebuildAttempt.status === 0) {
848
- state.recovered.push(recoveryCode);
849
- return await settleLaunch(
850
- jsonOutput,
851
- machine,
852
- stream,
853
- adapter,
854
- mobileTarget,
855
- tier,
856
- displayMode,
857
- target,
858
- state,
859
- wantWatch,
860
- wantVerify,
861
- remoteFlagRequest,
862
- false
863
- );
864
- }
865
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
866
- code: "EXTENSION_RELAUNCH_FAILED",
867
- message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
868
- recoverable: false,
869
- exitCode: EXIT.infra,
870
- originalError: rebuildAttempt.output.trim() || void 0
871
- });
872
- }
873
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
874
- code: "SIDEPANEL_OPEN_FAILED",
875
- message: "app launched but opening the side panel failed.",
876
- recoverable: false,
877
- userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
878
- exitCode: EXIT.runtime,
879
- originalError: sidepanel.output.trim() || void 0
880
- });
881
- }
882
- }
883
- if (adapter === "extension") {
884
- try {
885
- await ensureExtensionConsoleCapture(target);
886
- } catch (error) {
887
- if (!machine) {
888
- console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
889
- }
890
- }
891
- }
892
- if (wantVerify) {
893
- stream.phase("verify");
894
- const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
895
- if (adapter === "mobile" && mobileTarget) verifyArgs.push("--platform", mobileTarget);
896
- if (adapter === "extension" && process.env.CDP_PORT) {
897
- verifyArgs.push("--cdp-port", process.env.CDP_PORT);
898
- }
899
- if (machine) verifyArgs.push("--json");
900
- const exitCode = await handleHarness(verifyArgs);
901
- if (stream.enabled) {
902
- for (const mutation of state.mutations) stream.mutation(mutation);
903
- for (const recovery of state.recovered) stream.recovery(recovery);
904
- stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
905
- adapter,
906
- platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
907
- tier,
908
- recovered: state.recovered,
909
- mutations: state.mutations,
910
- metro: metroLaunchEvidence(adapter, target),
911
- remoteFeatureFlags: remoteFlags
912
- });
913
- }
914
- return exitCode;
915
- }
916
- return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state, remoteFlags);
917
- }
918
- function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state, remoteFlags = {}) {
919
- if (stream.enabled) {
920
- for (const mutation of state.mutations) stream.mutation(mutation);
921
- for (const recovery of state.recovered) stream.recovery(recovery);
922
- stream.complete("pass", EXIT.ok, {
923
- adapter,
924
- platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
925
- tier,
926
- recovered: state.recovered,
927
- mutations: state.mutations,
928
- metro: metroLaunchEvidence(adapter, target),
929
- remoteFeatureFlags: remoteFlags
930
- });
931
- } else if (json) {
932
- console.log(
933
- JSON.stringify(
934
- {
935
- schemaVersion: 1,
936
- command: "launch",
937
- adapter,
938
- platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
939
- tier,
940
- status: "pass",
941
- phase: "launch",
942
- recovered: state.recovered,
943
- mutations: state.mutations,
944
- metro: metroLaunchEvidence(adapter, target),
945
- remoteFeatureFlags: remoteFlags,
946
- exitCode: EXIT.ok
947
- },
948
- null,
949
- 2
950
- )
951
- );
952
- } else {
953
- const device = adapter === "mobile" ? launchDeviceLabel(mobileTarget) : displayMode;
954
- const tierNote = tier === "quick" ? "quick relaunch, no native build" : tier;
955
- const devNote = process.env.MM_HARNESS_BIN ? ` ${color("dim", "[dev: MM_HARNESS_BIN]")}` : "";
956
- console.error(
957
- `${color("ok", "\u2713")} launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} \u2014 ${color("ok", String(device))} \xB7 ${tierNote} \xB7 app + bridge ready${devNote}`
958
- );
959
- }
960
- return EXIT.ok;
961
- }
962
- function launchDeviceLabel(mobileTarget) {
963
- if (mobileTarget === "android") {
964
- return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
965
- }
966
- if (mobileTarget === "ios") {
967
- return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
968
- }
969
- return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
100
+ usage: (message, userAction) => launchUsage(jsonOutput, stream, message, userAction)
101
+ });
970
102
  }
971
103
  function launchUsage(json, stream, message, userAction) {
972
104
  stream.error({ code: "USAGE", message, userAction });
973
105
  return usageOut(json, "launch", message, userAction);
974
106
  }
975
- function launchFail(json, stream, adapter, mobileTarget, tier, state, target, failure) {
976
- const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
977
- if (stream.enabled) {
978
- for (const mutation of state.mutations) stream.mutation(mutation);
979
- for (const recovery of state.recovered) stream.recovery(recovery);
980
- stream.error({
981
- code: failure.code,
982
- message: failure.message,
983
- retryable: failure.recoverable,
984
- userAction,
985
- originalError: failure.originalError ?? null
986
- });
987
- stream.complete("fail", failure.exitCode, {
988
- adapter,
989
- platform: mobileTarget ?? null,
990
- tier,
991
- recovered: state.recovered,
992
- mutations: state.mutations,
993
- recoverable: failure.recoverable,
994
- attemptedRecoveries: state.attemptedRecoveries,
995
- metro: metroLaunchEvidence(adapter, target)
996
- });
997
- } else if (json) {
998
- console.log(
999
- JSON.stringify(
1000
- {
1001
- schemaVersion: 1,
1002
- command: "launch",
1003
- adapter,
1004
- platform: mobileTarget ?? null,
1005
- tier,
1006
- status: "fail",
1007
- phase: state.attemptedRecoveries.length ? "recover" : "launch",
1008
- recovered: state.recovered,
1009
- mutations: state.mutations,
1010
- recoverable: failure.recoverable,
1011
- attemptedRecoveries: state.attemptedRecoveries,
1012
- metro: metroLaunchEvidence(adapter, target),
1013
- exitCode: failure.exitCode,
1014
- error: {
1015
- code: failure.code,
1016
- message: failure.message,
1017
- retryable: failure.recoverable,
1018
- userAction,
1019
- originalError: failure.originalError ?? null
1020
- }
1021
- },
1022
- null,
1023
- 2
1024
- )
1025
- );
1026
- } else {
1027
- console.error(colorHumanMessage(
1028
- `\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
1029
- ${failure.message}` + (failure.originalError ? `
1030
- --- original failure ---
1031
- ${failure.originalError}` : "") + `
1032
- Next: ${userAction}`
1033
- ));
1034
- }
1035
- return failure.exitCode;
1036
- }
1037
107
  export {
1038
- TERMINAL_ONLY_LAUNCH_OPTIONS,
1039
- handleLaunch,
1040
- parseTerminalLauncherOutput,
1041
- terminalOnlyLaunchFlags
108
+ handleLaunch
1042
109
  };