@deeeed/metamask-harness 0.74.0 → 0.75.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.
Files changed (60) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/dist/action-values.js +15 -0
  5. package/dist/adapters/core/actions.js +20 -0
  6. package/dist/adapters/core/live-script-env.js +36 -0
  7. package/dist/adapters/core/run.js +54 -0
  8. package/dist/adapters/core/surface.js +5 -1
  9. package/dist/adapters/extension/action-inputs.js +206 -0
  10. package/dist/adapters/extension/actions.js +51 -0
  11. package/dist/adapters/extension/console-capture.js +13 -1
  12. package/dist/adapters/extension/launch.js +315 -0
  13. package/dist/adapters/extension/launched-browser.js +51 -0
  14. package/dist/adapters/extension/run.js +105 -0
  15. package/dist/adapters/extension/surface.js +9 -2
  16. package/dist/adapters/mobile/actions.js +54 -0
  17. package/dist/adapters/mobile/launch.js +192 -0
  18. package/dist/adapters/mobile/lifecycle.js +300 -0
  19. package/dist/adapters/mobile/run.js +156 -0
  20. package/dist/adapters/mobile/runtime-identity.js +34 -0
  21. package/dist/adapters/mobile/surface.js +11 -2
  22. package/dist/adapters/mobile/ui.js +701 -0
  23. package/dist/adapters/performance/cdp-trace.js +24 -6
  24. package/dist/adapters/performance/device-platform.js +10 -0
  25. package/dist/adapters/registry.js +1 -1
  26. package/dist/adapters/terminal/actions.js +43 -0
  27. package/dist/adapters/terminal/console-capture.js +14 -1
  28. package/dist/adapters/terminal/run.js +30 -0
  29. package/dist/adapters/terminal/surface.js +6 -1
  30. package/dist/adapters.js +32 -824
  31. package/dist/cli-commands.js +1 -0
  32. package/dist/command-contract.js +3 -0
  33. package/dist/commands/call.js +2 -5
  34. package/dist/commands/launch/device.js +2 -6
  35. package/dist/commands/launch/extension.js +3 -303
  36. package/dist/commands/launch/index.js +1 -1
  37. package/dist/commands/launch/mobile.js +2 -190
  38. package/dist/commands/launch/quote.js +7 -0
  39. package/dist/commands/pin.js +226 -0
  40. package/dist/commands/run-engine.js +38 -138
  41. package/dist/commands/run-report.js +4 -48
  42. package/dist/commands/run.js +6 -48
  43. package/dist/doctor.js +3 -1
  44. package/dist/execution-provenance.js +3 -2
  45. package/dist/harness-pin.js +10 -0
  46. package/dist/heal-bounds.js +3 -2
  47. package/dist/live-adapter-contract.js +8 -34
  48. package/dist/manifest.js +3 -5
  49. package/dist/metamask-action-validation.js +5 -196
  50. package/dist/mm-harness-cli.js +27 -2
  51. package/dist/network-observation.js +10 -3
  52. package/dist/performance-observation.js +15 -22
  53. package/dist/pr-body/render.js +3 -1
  54. package/dist/recording-target.js +4 -39
  55. package/dist/run-diagnostics.js +21 -60
  56. package/dist/run-recording.js +5 -4
  57. package/dist/runner.js +20 -368
  58. package/docs/CONTRIBUTING.md +19 -7
  59. package/package.json +2 -1
  60. package/scripts/completions.sh +2 -1
@@ -19,6 +19,7 @@ const SPEC = {
19
19
  { name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
20
20
  { name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
21
21
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
22
+ { name: "pin", desc: "Copy this install into an immutable store for a task to run", flags: ["--json"] },
22
23
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
23
24
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
24
25
  { name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
@@ -398,6 +398,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
398
398
  update: {
399
399
  options: options(HELP, JSON, { "--check": bool() })
400
400
  },
401
+ pin: {
402
+ options: options(HELP, JSON)
403
+ },
401
404
  fixtures: {
402
405
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
403
406
  "--from": value(),
@@ -28,7 +28,6 @@ import {
28
28
  import { getAdapterSurface } from "../adapters/surface.js";
29
29
  import { handleListExecutables } from "./list-executables.js";
30
30
  import { applyDeviceTargeting } from "./device-target.js";
31
- import { coreDependencyBlock } from "./core-readiness.js";
32
31
  import {
33
32
  emitHealViolation,
34
33
  executeWithHealBounds,
@@ -40,7 +39,6 @@ import {
40
39
  synthesizeOneNodeRecipe,
41
40
  validateRecipeAdapterAware
42
41
  } from "./run-engine.js";
43
- import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
44
42
  import { acquireCheckoutLock } from "../checkout-lock.js";
45
43
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
46
44
  import {
@@ -178,7 +176,7 @@ async function handleCall(argv) {
178
176
  const resolvedAction = resolution.resolved;
179
177
  const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
180
178
  const defaultsUsed = actionDefaultsUsed(describedAction, args);
181
- const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
179
+ const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, { action: resolvedAction }) ?? null;
182
180
  if (depsBlock) {
183
181
  if (json) {
184
182
  console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, status: "fail", exitCode: EXIT.usage, error: depsBlock }, null, 2));
@@ -286,9 +284,8 @@ async function handleCall(argv) {
286
284
  return checkoutBusyOut(json, "call", lock.message, lock.path);
287
285
  }
288
286
  try {
289
- const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
290
287
  const prepared = await prepareHeal(adapter, target, options, json, {
291
- skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
288
+ appRestartAuthored: resolvedAction === "app.lifecycle" && args.command === "restart"
292
289
  });
293
290
  if (typeof prepared === "number") return prepared;
294
291
  const { state } = prepared;
@@ -17,6 +17,7 @@ import {
17
17
  remoteFlagRequestFor
18
18
  } from "../../feature-flags.js";
19
19
  import { terminalOnlyLaunchFlags } from "../../adapters/terminal/launch.js";
20
+ import { shellQuote } from "./quote.js";
20
21
  async function runDeviceLaunch(context, profile) {
21
22
  const { adapter, options, target, heal, jsonOutput, machine, stream, usage } = context;
22
23
  const terminalOnly = terminalOnlyLaunchFlags(options);
@@ -182,10 +183,6 @@ function readinessBlockFailure(result) {
182
183
  exitCode: result.status
183
184
  };
184
185
  }
185
- function shellQuote(value) {
186
- if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
187
- return `'${value.replace(/'/gu, `'"'"'`)}'`;
188
- }
189
186
  function nativeInputsChanged(target, paths) {
190
187
  const baselineFile = recipeRuntimePath(target, ".last-build-ref");
191
188
  let baseline;
@@ -341,6 +338,5 @@ ${failure.originalError}` : "") + `
341
338
  return failure.exitCode;
342
339
  }
343
340
  export {
344
- runDeviceLaunch,
345
- shellQuote
341
+ runDeviceLaunch
346
342
  };
@@ -12,8 +12,7 @@ import {
12
12
  importRecipeHarnessRuntimeCdp,
13
13
  recipeHarnessPath,
14
14
  recipeRuntimeDir,
15
- runnerDir,
16
- walletFixturePath
15
+ runnerDir
17
16
  } from "../../paths.js";
18
17
  import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
19
18
  import {
@@ -30,16 +29,9 @@ import {
30
29
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
31
30
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
32
31
  import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
33
- import { EXIT, spawnScript, spawnScriptStreaming } from "../shared.js";
32
+ import { EXIT, spawnScriptStreaming } from "../shared.js";
34
33
  import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
35
- import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
36
- import {
37
- fixtureFeatureFlags,
38
- sameFeatureFlags,
39
- toRemoteFlagSpec
40
- } from "../../feature-flags.js";
41
- import { RECOVERY_CODE } from "../../heal-bounds.js";
42
- import { shellQuote } from "./device.js";
34
+ import { sameFeatureFlags } from "../../feature-flags.js";
43
35
  const { CdpSession } = await importRecipeHarnessRuntimeCdp();
44
36
  const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
45
37
  path.join(runnerDir, "adapters/extension/lib/chrome-args.cjs")
@@ -487,301 +479,9 @@ async function extensionRebuild(target, remoteFlags) {
487
479
  }
488
480
  return result;
489
481
  }
490
- const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
491
- function verifyExtensionPin(target, requested) {
492
- const pinned = readExtensionPinnedFlags(target);
493
- if (pinned.error) {
494
- return {
495
- ok: false,
496
- flags: {},
497
- code: "RUNTIME_MANIFEST_UNREADABLE",
498
- message: `the launched dist snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}`
499
- };
500
- }
501
- if (pinned.overrides !== null && pinned.pinned === null) {
502
- return {
503
- ok: false,
504
- flags: {},
505
- code: "REMOTE_FLAG_NOT_APPLIED",
506
- message: `the launched dist snapshot manifest (${pinned.sourcePath ?? "unknown path"}) carries no harness pin record; it was not written by this launch.`
507
- };
508
- }
509
- const actual = pinned.pinned ?? {};
510
- if (!sameFeatureFlags(actual, requested)) {
511
- const mismatched = [.../* @__PURE__ */ new Set([...Object.keys(requested), ...Object.keys(actual)])].filter(
512
- (key) => JSON.stringify(actual[key]) !== JSON.stringify(requested[key])
513
- );
514
- return {
515
- ok: false,
516
- flags: {},
517
- code: "REMOTE_FLAG_NOT_APPLIED",
518
- 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("; ")
519
- };
520
- }
521
- return { ok: true, flags: actual, message: "" };
522
- }
523
- function extensionQuickReattachFailed(output) {
524
- return /\[reattach\]|extension quick reattach|mm-harness launch --build/u.test(output);
525
- }
526
- function extensionRuntimeBlocked(output) {
527
- return /ERR_BLOCKED_BY_CLIENT|has been blocked by Chromium/u.test(output);
528
- }
529
- function extensionBrowserMissing(output) {
530
- return /Playwright Chromium is not installed|No compatible Chromium binary selected/u.test(output);
531
- }
532
- function extensionProductConfigMissing(output) {
533
- return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
534
- }
535
- function extensionEvmRpcUnreachable(output) {
536
- return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
537
- }
538
- const extensionLaunchProfile = {
539
- refuseFlags({ target }, { watch, runway, clearMetro, remoteFlagSpec }) {
540
- if (runway) return { message: "runway is mobile-only.", userAction: "drop --runway for the extension" };
541
- if (clearMetro) return { message: "--clear-metro is mobile-only.", userAction: "drop --clear-metro for the extension" };
542
- if (remoteFlagSpec !== void 0 && watch) {
543
- return {
544
- message: "--remote-flag cannot be applied with --watch: the watcher never re-creates the dist snapshot that carries the pin.",
545
- userAction: `mm-harness launch --adapter extension --remote-flag ${remoteFlagSpec} --target ${shellQuote(target)}`
546
- };
547
- }
548
- return void 0;
549
- },
550
- // --url: initial main-tab page for extension launches (dapp beside the
551
- // sidepanel); travels to live.sh/launch-browser via env. Sidepanel mode has a
552
- // useful default so `mm-harness launch --sidepanel` opens a working dapp, not
553
- // just a wallet panel with no page to connect from.
554
- start({ options, displayMode, machine, wantWatch, tier }) {
555
- const requestedUrl = typeof options["url"] === "string" && options["url"] ? options["url"] : "";
556
- if (requestedUrl || displayMode === "sidepanel") {
557
- process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
558
- }
559
- if (machine) return;
560
- const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
561
- const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
562
- console.error(colorHumanMessage(
563
- `\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
564
- ));
565
- },
566
- nativeInputs: ["package.json", "yarn.lock", "webpack.config.js"],
567
- rebuildCommand: () => "mm-harness launch --build",
568
- // Source-development launch paths require checkout tooling. A verified local
569
- // release artifact can reuse its healthy browser without building or watching.
570
- async healthcheck(launch, state) {
571
- const { target, tier, wantWatch, heal, stream } = launch;
572
- const artifact = releaseArtifactState(target);
573
- const reusable = !wantWatch && tier !== "build" && await extensionRuntimeReusable(target, artifact);
574
- const verifiedReleaseReuse = artifact.status === "valid" && reusable;
575
- const verifiedReleaseLaunch = !wantWatch && tier !== "build" && artifact.status === "valid" && runtimeDistCheck(target, artifact).status === "fresh";
576
- const willBuild = !reusable;
577
- if (willBuild) {
578
- const productConfigBlock = extensionProductConfigBlock(target);
579
- if (productConfigBlock) {
580
- return launch.fail(state, {
581
- code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
582
- message: productConfigBlock.message,
583
- recoverable: false,
584
- userAction: productConfigBlock.userAction,
585
- exitCode: EXIT.infra
586
- });
587
- }
588
- }
589
- const depsBlock = verifiedReleaseReuse || verifiedReleaseLaunch ? null : extensionDepsBlock(target);
590
- if (depsBlock) {
591
- if (heal === "off") return launch.usage(depsBlock.message, depsBlock.userAction);
592
- const recoveryCode = "deps.installed";
593
- state.attemptedRecoveries.push(recoveryCode);
594
- stream.phase("recover");
595
- const repaired = await installExtensionDeps(target);
596
- if (repaired.status !== 0 || extensionDepsBlock(target)) {
597
- return launch.fail(state, {
598
- code: "DEPENDENCY_INSTALL_FAILED",
599
- message: depsBlock.message,
600
- recoverable: false,
601
- userAction: "install the checkout-pinned Node version, then re-run mm-harness launch",
602
- originalError: repaired.output,
603
- exitCode: EXIT.infra
604
- });
605
- }
606
- state.recovered.push(recoveryCode);
607
- state.mutations.push({ type: "dependencies", action: "installed", path: target });
608
- }
609
- return void 0;
610
- },
611
- compose: ({ target, wantWatch, displayMode, remoteFlagRequest }, tier) => launchExtension(target, tier, wantWatch, displayMode, remoteFlagRequest),
612
- classify({ target, remoteFlagRequest }, output) {
613
- if (output.includes("REMOTE_FLAG_NOT_PINNABLE:")) {
614
- return {
615
- code: "REMOTE_FLAG_NOT_PINNABLE",
616
- 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.`,
617
- recoverable: false,
618
- // The next step must leave the artifact alone: a --build here would replace
619
- // the artifact under validation with a source build.
620
- 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)}`,
621
- exitCode: EXIT.infra,
622
- originalError: output.trim() || void 0
623
- };
624
- }
625
- if (extensionProductConfigMissing(output)) {
626
- const block = extensionProductConfigBlock(target);
627
- return {
628
- code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
629
- message: block?.message ?? "Extension product configuration was not ready when the build started.",
630
- recoverable: false,
631
- userAction: block?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
632
- exitCode: EXIT.infra
633
- };
634
- }
635
- if (extensionBrowserMissing(output)) {
636
- return {
637
- code: "EXTENSION_BROWSER_REQUIRED",
638
- message: "the checkout Playwright browser is not installed; system Chrome is not a compatible unpacked-extension fallback. Installing the browser cache requires user approval.",
639
- recoverable: false,
640
- userAction: `ask the user for approval, then run: cd ${shellQuote(target)} && yarn playwright install chromium`,
641
- exitCode: EXIT.infra
642
- };
643
- }
644
- if (output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") || output.includes("EXTENSION_STARTUP_UNVERIFIED:")) {
645
- return {
646
- code: output.includes("EXTENSION_BACKGROUND_UNRESPONSIVE:") ? "EXTENSION_BACKGROUND_UNRESPONSIVE" : "EXTENSION_STARTUP_UNVERIFIED",
647
- message: "Extension startup is not healthy; inspect the background error before restarting or resetting wallet state.",
648
- recoverable: false,
649
- userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
650
- exitCode: EXIT.runtime,
651
- originalError: output.trim() || void 0
652
- };
653
- }
654
- if (output.includes("EVM_RPC_EXECUTION_FAILED:")) {
655
- return {
656
- code: "EVM_RPC_EXECUTION_FAILED",
657
- message: "Background RPC fetch failed during JavaScript execution; this does not establish an Infura authentication or connectivity failure.",
658
- recoverable: false,
659
- userAction: `mm-harness logs --target ${shellQuote(target)} --full`,
660
- exitCode: EXIT.runtime,
661
- originalError: output.trim() || void 0
662
- };
663
- }
664
- if (extensionEvmRpcUnreachable(output)) {
665
- return {
666
- code: "EVM_RPC_UNREACHABLE",
667
- message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
668
- recoverable: false,
669
- 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)}`,
670
- exitCode: EXIT.runtime,
671
- originalError: output.trim() || void 0
672
- };
673
- }
674
- return void 0;
675
- },
676
- async beforeHeal(launch, attempt, state) {
677
- if (launch.tier !== "quick" || !extensionQuickReattachFailed(attempt.output)) return void 0;
678
- const recoveryCode = "chrome.relaunched";
679
- state.attemptedRecoveries.push(recoveryCode);
680
- launch.stream.phase("recover");
681
- const rebuildAttempt = await launch.compose("build");
682
- if (rebuildAttempt.status === 0) {
683
- state.recovered.push(recoveryCode);
684
- return await launch.settle(state, rebuildAttempt.remoteFeatureFlags);
685
- }
686
- return launch.fail(state, {
687
- code: "EXTENSION_RELAUNCH_FAILED",
688
- message: "quick reattach failed, and clean relaunch failed too.",
689
- recoverable: false,
690
- exitCode: EXIT.infra,
691
- originalError: rebuildAttempt.output.trim() || void 0
692
- });
693
- },
694
- recovery: () => ({ code: RECOVERY_CODE.extension, restartApp: false }),
695
- // --watch only restarts the webpack watcher; the snapshot, and its pin, are
696
- // untouched, so the live record is reported rather than verified against a
697
- // request the watcher could never apply (--remote-flag is refused with it;
698
- // the fixture baseline stays whatever the last launch pinned).
699
- provenFlags({ target, wantWatch, remoteFlagRequest }, requested) {
700
- if (wantWatch) return { flags: readExtensionPinnedFlags(target).pinned };
701
- const pinned = verifyExtensionPin(target, requested);
702
- if (pinned.ok) return { flags: pinned.flags };
703
- const spec = remoteFlagRequest?.spec;
704
- return {
705
- code: pinned.code ?? "REMOTE_FLAG_NOT_APPLIED",
706
- message: pinned.message,
707
- recoverable: false,
708
- // A rebuild re-creates the snapshot; the flag is repeated only when one was asked for.
709
- userAction: `mm-harness launch --adapter extension --build${spec ? ` --remote-flag ${spec}` : ""} --target ${shellQuote(target)}`,
710
- exitCode: EXIT.runtime
711
- };
712
- },
713
- async finish(launch, state, sidepanelRecoveryAllowed) {
714
- const { target, machine, displayMode, tier, stream } = launch;
715
- const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
716
- if (displayMode === "fullscreen") {
717
- const sidepanelArgs = ["close"];
718
- if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
719
- const fullscreen = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
720
- if (fullscreen.status !== 0) {
721
- return launch.fail(state, {
722
- code: "FULLSCREEN_OPEN_FAILED",
723
- message: "app launched but restoring the fullscreen Extension surface failed.",
724
- recoverable: false,
725
- userAction: `bash adapters/extension/sidepanel-toggle.sh close --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
726
- exitCode: EXIT.runtime,
727
- originalError: fullscreen.output.trim() || void 0
728
- });
729
- }
730
- }
731
- if (displayMode === "sidepanel") {
732
- const sidepanelArgs = ["open"];
733
- if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
734
- if (!machine) {
735
- console.error(
736
- `[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
737
- );
738
- }
739
- const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
740
- if (sidepanel.status !== 0) {
741
- const recoveryCode = "chrome.relaunched";
742
- if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
743
- state.attemptedRecoveries.push(recoveryCode);
744
- stream.phase("recover");
745
- const rebuildAttempt = await launch.compose("build");
746
- if (rebuildAttempt.status === 0) {
747
- state.recovered.push(recoveryCode);
748
- return await launch.settle(state, void 0, false);
749
- }
750
- return launch.fail(state, {
751
- code: "EXTENSION_RELAUNCH_FAILED",
752
- message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
753
- recoverable: false,
754
- exitCode: EXIT.infra,
755
- originalError: rebuildAttempt.output.trim() || void 0
756
- });
757
- }
758
- return launch.fail(state, {
759
- code: "SIDEPANEL_OPEN_FAILED",
760
- message: "app launched but opening the side panel failed.",
761
- recoverable: false,
762
- userAction: `bash adapters/extension/sidepanel-toggle.sh open --cdp-port ${process.env.CDP_PORT ?? "<CDP_PORT>"}`,
763
- exitCode: EXIT.runtime,
764
- originalError: sidepanel.output.trim() || void 0
765
- });
766
- }
767
- }
768
- try {
769
- await ensureExtensionConsoleCapture(target);
770
- } catch (error) {
771
- if (!machine) {
772
- console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
773
- }
774
- }
775
- return void 0;
776
- },
777
- verifyArgs: () => process.env.CDP_PORT ? ["--cdp-port", process.env.CDP_PORT] : [],
778
- platformLabel: ({ mobileTarget, displayMode }) => mobileTarget ?? displayMode,
779
- deviceLabel: ({ displayMode }) => displayMode
780
- };
781
482
  export {
782
483
  extensionDepsBlock,
783
484
  extensionLaunchDevelopment,
784
- extensionLaunchProfile,
785
485
  extensionReattach,
786
486
  extensionRebuild,
787
487
  extensionRuntimeReusable,
@@ -14,7 +14,7 @@ import {
14
14
  import { parseHeal } from "../../heal-bounds.js";
15
15
  import { acquireCheckoutLock } from "../../checkout-lock.js";
16
16
  import { JsonStreamWriter } from "../../json-stream.js";
17
- import { shellQuote } from "./device.js";
17
+ import { shellQuote } from "./quote.js";
18
18
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
19
19
  "build",
20
20
  "watch",
@@ -1,4 +1,3 @@
1
- import fs from "node:fs";
2
1
  import { prepareMobile } from "../../adapters/mobile/prepare.js";
3
2
  import {
4
3
  mobileSourceCheck,
@@ -8,11 +7,7 @@ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
8
7
  import { pinMobileFeatureFlags } from "../../adapters/mobile/feature-flags.js";
9
8
  import { toRemoteFlagSpec } from "../../feature-flags.js";
10
9
  import { clearMobileReleaseArtifactState } from "../../adapters/mobile/release-artifact-state.js";
11
- import { EXIT, str, writeInteractiveProgress } from "../shared.js";
12
- import { applyDeviceTargeting } from "../device-target.js";
13
- import { RECOVERY_CODE } from "../../heal-bounds.js";
14
- import { recipeRuntimePath } from "../../paths.js";
15
- import { shellQuote } from "./device.js";
10
+ import { EXIT } from "../shared.js";
16
11
  async function launchMobile(target, mobileTarget, tier, json, restartApp = false, clearMetro = false, remoteFlags) {
17
12
  clearMobileReleaseArtifactState(target);
18
13
  await ensureHarnessFresh(target, "mobile");
@@ -59,189 +54,6 @@ async function launchMobile(target, mobileTarget, tier, json, restartApp = false
59
54
  }
60
55
  return { ...result, remoteFeatureFlags: provenFlags };
61
56
  }
62
- function mobileProvisioningBlocked(output) {
63
- 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);
64
- }
65
- function missingConfiguredIosSimulator(output) {
66
- return /open-device: configured iOS simulator '([^']+)' does not exist/u.exec(output)?.[1];
67
- }
68
- function ambiguousConfiguredIosSimulator(output) {
69
- return /open-device: iOS simulator target ("[^"]+") is ambiguous/u.exec(output)?.[1];
70
- }
71
- function mobileBridgeTargetMissing(output) {
72
- return /no bridge target matched|wait-for-bridge: readiness deadline expired/iu.test(output);
73
- }
74
- function mobileProvisionCommand(target, mobileTarget) {
75
- const platform = mobileTarget === "android" ? "android" : "ios";
76
- const device = platform === "ios" ? process.env.SIM_UDID || process.env.IOS_SIMULATOR : process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE;
77
- const parts = ["mm-harness", "provision", "runway", platform, "--adapter", "mobile", "--target", shellQuote(target)];
78
- if (device) parts.push("--device", shellQuote(device));
79
- return parts.join(" ");
80
- }
81
- function isIosSimulatorUdid(value) {
82
- return /^[0-9A-Fa-f-]{36}$/u.test(value);
83
- }
84
- function launchDeviceLabel(mobileTarget) {
85
- if (mobileTarget === "android") {
86
- return process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || process.env.ANDROID_DEVICE || "android device";
87
- }
88
- if (mobileTarget === "ios") {
89
- return process.env.IOS_SIMULATOR || process.env.SIM_UDID || "iOS simulator";
90
- }
91
- return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
92
- }
93
- function metroLaunchEvidence(target) {
94
- const evidencePath = recipeRuntimePath(target, "metro-generation.json");
95
- let descriptor;
96
- try {
97
- descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
98
- const stat = fs.fstatSync(descriptor);
99
- if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
100
- return JSON.parse(fs.readFileSync(descriptor, "utf8"));
101
- } catch {
102
- return void 0;
103
- } finally {
104
- if (descriptor !== void 0) fs.closeSync(descriptor);
105
- }
106
- }
107
- const mobileLaunchProfile = {
108
- refuseFlags({ mobileTarget }) {
109
- if (!mobileTarget) {
110
- return { message: "target is required for mobile.", userAction: "mm-harness launch ios or mm-harness launch android" };
111
- }
112
- return void 0;
113
- },
114
- applyEnv({ adapter, options, mobileTarget, target }) {
115
- const device = str(options, "device");
116
- if (!device && mobileTarget !== "ios" && mobileTarget !== "android") return void 0;
117
- const targetingOptions = {
118
- ...options,
119
- ...mobileTarget ? { platform: mobileTarget } : {}
120
- };
121
- const result = applyDeviceTargeting("launch", adapter, targetingOptions, { gate: false, rerun: "" });
122
- if ("code" in result) {
123
- if (mobileTarget === "ios" && result.code === "DEVICE_NOT_FOUND") {
124
- process.env.IOS_SIMULATOR = device;
125
- process.env.SIM_UDID = isIosSimulatorUdid(device) ? device : "";
126
- process.env.ADB_SERIAL = "";
127
- process.env.ANDROID_SERIAL = "";
128
- process.env.ANDROID_DEVICE = "";
129
- process.env.ANDROID_TARGET_DEVICE_NAME = "";
130
- } else {
131
- return result;
132
- }
133
- }
134
- if (mobileTarget === "android" && device && !process.env.ADB_SERIAL && !process.env.ANDROID_SERIAL) {
135
- return {
136
- ok: false,
137
- code: "DEVICE_WRONG_PLATFORM",
138
- message: `--device ${device} did not resolve to an Android device for launch android.`,
139
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
140
- };
141
- }
142
- if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
143
- return {
144
- ok: false,
145
- code: "DEVICE_WRONG_PLATFORM",
146
- message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
147
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
148
- };
149
- }
150
- return void 0;
151
- },
152
- start({ options, mobileTarget, machine, tier, wantClearMetro }) {
153
- 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";
154
- const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
155
- writeInteractiveProgress(
156
- machine,
157
- `\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}${wantClearMetro ? " \xB7 clear cache" : ""}`
158
- );
159
- },
160
- nativeInputs: ["ios", "android", "package.json", "yarn.lock"],
161
- rebuildCommand: ({ mobileTarget }) => `mm-harness launch ${mobileTarget} --build`,
162
- compose: ({ target, mobileTarget, machine, remoteFlagRequest }, tier, restartApp, clearMetro) => launchMobile(target, mobileTarget, tier, machine, restartApp, clearMetro, remoteFlagRequest?.flags),
163
- classify(_launch, output) {
164
- if (output.includes("open-device: Android device is locked.")) {
165
- return {
166
- code: "MOBILE_DEVICE_LOCKED",
167
- message: "the selected Android device is locked.",
168
- recoverable: false,
169
- userAction: "Unlock the selected Android device, then retry the launch.",
170
- exitCode: EXIT.runtime,
171
- originalError: output.trim()
172
- };
173
- }
174
- if (/open-device: Android (?:lock state unavailable|display preparation failed)\./u.test(output)) {
175
- return {
176
- code: "MOBILE_DEVICE_UI_UNAVAILABLE",
177
- message: "the selected Android display could not be prepared.",
178
- recoverable: false,
179
- userAction: "Check adb access and the selected device screen, then retry the launch.",
180
- exitCode: EXIT.runtime,
181
- originalError: output.trim()
182
- };
183
- }
184
- return void 0;
185
- },
186
- async beforeHeal(launch, attempt, state) {
187
- const { target, mobileTarget, tier } = launch;
188
- const output = attempt.output;
189
- const missingIosSimulator = missingConfiguredIosSimulator(output);
190
- const ambiguousIosSimulator = ambiguousConfiguredIosSimulator(output);
191
- if (ambiguousIosSimulator) {
192
- return launch.fail(state, {
193
- code: "MOBILE_DEVICE_AMBIGUOUS",
194
- message: `more than one iOS simulator matches ${ambiguousIosSimulator}.`,
195
- recoverable: false,
196
- userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
197
- exitCode: EXIT.runtime,
198
- originalError: output.trim() || void 0
199
- });
200
- }
201
- if (missingIosSimulator && isIosSimulatorUdid(missingIosSimulator)) {
202
- return launch.fail(state, {
203
- code: "MOBILE_DEVICE_NOT_FOUND",
204
- message: `the configured iOS simulator ${missingIosSimulator} no longer exists.`,
205
- recoverable: false,
206
- userAction: `mm-harness status --adapter mobile --target ${shellQuote(target)} --all-devices --json`,
207
- exitCode: EXIT.runtime,
208
- originalError: output.trim() || void 0
209
- });
210
- }
211
- if (mobileProvisioningBlocked(output)) {
212
- return launch.fail(state, {
213
- code: "MOBILE_PROVISION_REQUIRED",
214
- message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
215
- recoverable: false,
216
- userAction: mobileProvisionCommand(target, mobileTarget),
217
- exitCode: EXIT.runtime,
218
- originalError: output.trim() || void 0
219
- });
220
- }
221
- if (tier === "build" && /open-device: (?:Android|iOS) native build failed/u.test(output)) {
222
- return launch.fail(state, {
223
- code: "MOBILE_NATIVE_BUILD_FAILED",
224
- message: "the selected Mobile native build failed; refusing to repeat the expensive build as a Metro recovery.",
225
- recoverable: false,
226
- exitCode: EXIT.runtime,
227
- originalError: output.trim() || void 0
228
- });
229
- }
230
- return void 0;
231
- },
232
- recovery(_launch, output) {
233
- const restartApp = mobileBridgeTargetMissing(output);
234
- return { code: restartApp ? "mobile.app-restarted" : RECOVERY_CODE.mobile, restartApp };
235
- },
236
- // Mobile proved its set inside launchMobile (null: nothing requested and no
237
- // runtime answered).
238
- provenFlags: (_launch, requested, proven) => ({ flags: proven === void 0 ? requested : proven }),
239
- verifyArgs: ({ mobileTarget }) => mobileTarget ? ["--platform", mobileTarget] : [],
240
- platformLabel: ({ mobileTarget }) => mobileTarget ?? null,
241
- deviceLabel: ({ mobileTarget }) => launchDeviceLabel(mobileTarget),
242
- evidence: metroLaunchEvidence
243
- };
244
57
  export {
245
- launchMobile,
246
- mobileLaunchProfile
58
+ launchMobile
247
59
  };
@@ -0,0 +1,7 @@
1
+ function shellQuote(value) {
2
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(value)) return value;
3
+ return `'${value.replace(/'/gu, `'"'"'`)}'`;
4
+ }
5
+ export {
6
+ shellQuote
7
+ };