@deeeed/metamask-harness 0.75.0 → 0.76.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 (45) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/adapters/shared/reap-checkout-metros.sh +9 -0
  3. package/dist/adapters/core/overlay.js +34 -0
  4. package/dist/adapters/core/readiness.js +8 -0
  5. package/dist/adapters/core/surface.js +7 -0
  6. package/dist/adapters/extension/doctor-checks.js +135 -0
  7. package/dist/adapters/extension/fixtures.js +103 -0
  8. package/dist/adapters/extension/overlay.js +55 -0
  9. package/dist/adapters/extension/readiness.js +32 -0
  10. package/dist/adapters/extension/reload.js +40 -0
  11. package/dist/adapters/extension/surface.js +22 -1
  12. package/dist/adapters/harness-freshness.js +4 -2
  13. package/dist/adapters/mobile/devices.js +21 -0
  14. package/dist/adapters/mobile/doctor-checks.js +40 -0
  15. package/dist/adapters/mobile/fixtures.js +141 -0
  16. package/dist/adapters/mobile/overlay.js +27 -0
  17. package/dist/adapters/mobile/prepare-target.js +38 -0
  18. package/dist/adapters/mobile/readiness.js +39 -0
  19. package/dist/adapters/mobile/reload.js +15 -0
  20. package/dist/adapters/mobile/surface.js +28 -1
  21. package/dist/adapters/terminal/doctor-checks.js +22 -0
  22. package/dist/adapters/terminal/overlay.js +18 -0
  23. package/dist/adapters/terminal/readiness.js +11 -0
  24. package/dist/adapters/terminal/surface.js +15 -2
  25. package/dist/cli.js +3 -1
  26. package/dist/commands/debug.js +6 -25
  27. package/dist/commands/device-target.js +2 -1
  28. package/dist/commands/doctor.js +26 -137
  29. package/dist/commands/fixtures.js +13 -217
  30. package/dist/commands/logs.js +1 -1
  31. package/dist/commands/parse-args.js +9 -4
  32. package/dist/commands/prepare.js +23 -53
  33. package/dist/commands/provision.js +3 -7
  34. package/dist/commands/reload.js +8 -47
  35. package/dist/commands/runtime-decision.js +5 -27
  36. package/dist/commands/self-test.js +1 -3
  37. package/dist/commands/status.js +20 -28
  38. package/dist/commands/stop.js +1 -1
  39. package/dist/doctor.js +9 -119
  40. package/dist/harness.js +31 -100
  41. package/dist/mm-harness-cli.js +3 -2
  42. package/dist/paths.js +8 -2
  43. package/dist/runtime-context.js +12 -17
  44. package/docs/CONTRIBUTING.md +27 -17
  45. package/package.json +1 -1
@@ -0,0 +1,141 @@
1
+ import path from "node:path";
2
+ import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
3
+ import { runnerDir } from "../../paths.js";
4
+ import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
5
+ const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
6
+ const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
7
+ async function applyMobileFixture({
8
+ target,
9
+ sub,
10
+ fixturePath,
11
+ canonicalFixture,
12
+ fixtureFlags,
13
+ json,
14
+ retryHint
15
+ }) {
16
+ const adapter = "mobile";
17
+ const fixtureFlagsJson = JSON.stringify(fixtureFlags);
18
+ const reset = sub === "reset";
19
+ process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
20
+ `);
21
+ let status;
22
+ let mobileSetupResult;
23
+ let resetError;
24
+ const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
25
+ const previousAppRoot = process.env.APP_ROOT;
26
+ process.env.APP_ROOT = target;
27
+ try {
28
+ if (reset) {
29
+ const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
30
+ const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
31
+ const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
32
+ const resetArgs = ["--platform", platform, "--target", target];
33
+ if (platform === "ios") {
34
+ resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
35
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
36
+ resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
37
+ }
38
+ const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
39
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
40
+ });
41
+ if (resetResult.status !== 0) {
42
+ resetError = new Error("Mobile app data reset failed.");
43
+ } else {
44
+ const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
45
+ const prewarmResult = await spawnScriptStreaming(
46
+ prewarmBundle,
47
+ ["--platform", platform, "--target", target, "--port", port],
48
+ target,
49
+ {
50
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
51
+ }
52
+ );
53
+ if (prewarmResult.status !== 0) {
54
+ resetError = new Error("Mobile bundle prewarm failed after app data reset.");
55
+ }
56
+ }
57
+ if (!resetError) {
58
+ const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
59
+ const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
60
+ if (platform === "ios") {
61
+ openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
62
+ } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
63
+ openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
64
+ }
65
+ const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
66
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
67
+ });
68
+ if (openResult.status !== 0) {
69
+ resetError = new Error("Mobile app relaunch after data reset failed.");
70
+ } else {
71
+ const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
72
+ const waitResult = await spawnScriptStreaming(
73
+ waitForBridge,
74
+ ["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
75
+ target,
76
+ { timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
77
+ );
78
+ if (waitResult.status !== 0) {
79
+ resetError = new Error("Mobile bridge did not return after app data reset.");
80
+ }
81
+ }
82
+ }
83
+ }
84
+ const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_SETUP_WALLET_TIMEOUT_MS);
85
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_SETUP_WALLET_TIMEOUT_MS;
86
+ if (!resetError) {
87
+ mobileSetupResult = await spawnScriptStreaming(
88
+ setupWalletSh,
89
+ ["--fixture", fixturePath, "--remote-flags-json", fixtureFlagsJson],
90
+ target,
91
+ {
92
+ env: {
93
+ APP_ROOT: target,
94
+ WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
95
+ METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
96
+ },
97
+ timeoutMs
98
+ }
99
+ );
100
+ }
101
+ status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
102
+ } finally {
103
+ if (previousAppRoot === void 0) delete process.env.APP_ROOT;
104
+ else process.env.APP_ROOT = previousAppRoot;
105
+ }
106
+ if (!json && status === "fail") {
107
+ console.error(` Next: ${retryHint}`);
108
+ }
109
+ const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
110
+ if (json) {
111
+ console.log(
112
+ JSON.stringify(
113
+ {
114
+ schemaVersion: 1,
115
+ command: "fixtures",
116
+ action: sub,
117
+ adapter,
118
+ fixture: fixturePath,
119
+ canonicalFixture,
120
+ status,
121
+ exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
122
+ walletReset: reset && status === "pass",
123
+ message: teaching,
124
+ error: status === "fail" ? {
125
+ code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
126
+ message: resetError ? `wallet reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}` : mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
127
+ userAction: retryHint
128
+ } : null
129
+ },
130
+ null,
131
+ 2
132
+ )
133
+ );
134
+ } else if (status === "pass") {
135
+ console.error(teaching);
136
+ }
137
+ return status === "pass" ? EXIT.ok : EXIT.runtime;
138
+ }
139
+ export {
140
+ applyMobileFixture
141
+ };
@@ -0,0 +1,27 @@
1
+ import { mobilePlatformFromConfiguredTarget, mobilePlatformFromDevices, readRuntimeContextTarget } from "./prepare-target.js";
2
+ const mobileHarness = {
3
+ install: { entry: "adapters/mobile/inject.sh", fallback: "scripts/inject-mobile-harness.sh" },
4
+ cleanup: { entry: "adapters/mobile/cleanup.sh", fallback: "scripts/cleanup-mobile-harness.sh", fromInstalledRunner: true },
5
+ // Runner-owned typed code, not a shipped leaf: no installed overlay copy can shadow it.
6
+ verify: () => ({ inProcess: async () => (await import("./verify.js")).runMobileVerify }),
7
+ restart: "mm-harness launch ios # or launch android"
8
+ };
9
+ const mobileRuntimeContext = {
10
+ forbiddenFields: ["cdpPort", "extensionId"],
11
+ localPorts: { watcher: 4e4 },
12
+ resources: ({ watcherPort, existing }) => ({
13
+ metroPort: watcherPort,
14
+ simulator: process.env.IOS_SIMULATOR || existing.simulator,
15
+ simulatorUdid: process.env.SIM_UDID || existing.simulatorUdid,
16
+ adbSerial: process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || existing.adbSerial
17
+ })
18
+ };
19
+ const mobilePrepare = {
20
+ clearMetro: true,
21
+ devicePlatform: (target, devices) => mobilePlatformFromConfiguredTarget(process.env, readRuntimeContextTarget(target)) ?? mobilePlatformFromDevices(devices)
22
+ };
23
+ export {
24
+ mobileHarness,
25
+ mobilePrepare,
26
+ mobileRuntimeContext
27
+ };
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recipeRuntimeDir } from "../../paths.js";
4
+ function readRuntimeContextTarget(target) {
5
+ const file = path.join(target, recipeRuntimeDir(), "agentic-runtime.json");
6
+ let parsed;
7
+ try {
8
+ parsed = JSON.parse(fs.readFileSync(file, "utf8"));
9
+ } catch {
10
+ return {};
11
+ }
12
+ const record = parsed ?? {};
13
+ const str = (key) => typeof record[key] === "string" ? record[key] : void 0;
14
+ return { simulator: str("simulator"), simulatorUdid: str("simulatorUdid"), adbSerial: str("adbSerial") };
15
+ }
16
+ function mobilePlatformFromConfiguredTarget(env, context) {
17
+ const ios = Boolean(env.IOS_SIMULATOR || env.SIM_UDID || context.simulator || context.simulatorUdid);
18
+ const android = Boolean(
19
+ env.ADB_SERIAL || env.ANDROID_SERIAL || env.ANDROID_TARGET_DEVICE_NAME || env.ANDROID_DEVICE || context.adbSerial
20
+ );
21
+ if (ios === android) return void 0;
22
+ return ios ? "ios" : "android";
23
+ }
24
+ function mobilePlatformFromDevices(devices) {
25
+ const selected = devices.filter((device) => device?.selected === true);
26
+ const candidates = selected.length > 0 ? selected : devices;
27
+ const platforms = [
28
+ ...new Set(
29
+ candidates.map((device) => device?.platform).filter((value) => value === "ios" || value === "android")
30
+ )
31
+ ];
32
+ return platforms.length === 1 ? platforms[0] : void 0;
33
+ }
34
+ export {
35
+ mobilePlatformFromConfiguredTarget,
36
+ mobilePlatformFromDevices,
37
+ readRuntimeContextTarget
38
+ };
@@ -0,0 +1,39 @@
1
+ import { optionFlag, optionString, parsePort } from "../../commands/parse-args.js";
2
+ import { detectOrphanMetros, mobileToolDoctorChecks } from "./doctor-checks.js";
3
+ import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "./perps-env.js";
4
+ const mobileReadiness = {
5
+ compatibilityMode(_target, shape) {
6
+ if (shape.injectedHarness && shape.agenticService) return "runner bridge with injected app bridge";
7
+ if (shape.agenticService) return "runner bridge with app bridge";
8
+ return "runner bridge available; app bridge not installed";
9
+ },
10
+ checks: (_target, platform) => mobileToolDoctorChecks(platform === "ios" || platform === "android" ? platform : void 0),
11
+ environment: (target) => ({ mobilePerps: mobilePerpsEnvironment(target) }),
12
+ lines(target) {
13
+ const perps = mobilePerpsEnvironment(target);
14
+ return [
15
+ `mobile-perps: ${perps.status} (${perps.file}; MM_PERPS_ENABLED=${perps.perpsEnabled.value ?? "missing"}; OVERRIDE_REMOTE_FEATURE_FLAGS=${perps.remoteFeatureFlagOverride.value ?? "missing"})`
16
+ ];
17
+ },
18
+ orphanDevServers: detectOrphanMetros,
19
+ captureProviders: ["simctl", "adb"],
20
+ fixes: [{ id: "mobile-perps-env", apply: (target) => ensureMobilePerpsEnvironment(target).length > 0 }],
21
+ devices: async () => (await import("./devices.js")).mobileDevices
22
+ };
23
+ async function mobileRuntimeDecision(target, options) {
24
+ const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
25
+ const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
26
+ const { decideMobileReadiness } = await import("./runtime-decision.js");
27
+ const report = await decideMobileReadiness(target, {
28
+ watcherPort,
29
+ metroLog: optionString(options, "metroLog"),
30
+ platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
31
+ record: optionFlag(options, "record"),
32
+ preflightMode: optionString(options, "preflightMode")
33
+ });
34
+ return { report, line: `${report.decision} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}` };
35
+ }
36
+ export {
37
+ mobileReadiness,
38
+ mobileRuntimeDecision
39
+ };
@@ -0,0 +1,15 @@
1
+ import path from "node:path";
2
+ import { EXIT, spawnScript } from "../../commands/shared.js";
3
+ import { runnerDir } from "../../paths.js";
4
+ function reloadMobile(target, json) {
5
+ const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
6
+ const args = [];
7
+ if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
8
+ if (json) args.push("--json");
9
+ const result = spawnScript(process.execPath, [script, ...args], target, json);
10
+ if (json) process.stdout.write(result.output);
11
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
12
+ }
13
+ export {
14
+ reloadMobile
15
+ };
@@ -6,7 +6,11 @@ import { resolveMobileSlotPorts } from "../slot-ports.js";
6
6
  import { mobileRuntimeStatus } from "./prepare.js";
7
7
  import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
8
8
  import { adapterRegistry } from "../registry.js";
9
+ import { writeInteractiveProgress } from "../../commands/shared.js";
9
10
  import { mobileActions } from "./actions.js";
11
+ import { mobileHarness, mobilePrepare, mobileRuntimeContext } from "./overlay.js";
12
+ import { mobileReadiness, mobileRuntimeDecision } from "./readiness.js";
13
+ import { clearMobileReleaseArtifactState } from "./release-artifact-state.js";
10
14
  import { mobileDiagnostics, mobileObservation, mobileRecording, mobileRun } from "./run.js";
11
15
  import { mobileSourceFingerprint } from "./source-freshness.js";
12
16
  function mobileRuntimeNextAction(reasonCode, target, platform) {
@@ -37,7 +41,12 @@ const mobileSurface = {
37
41
  runwayProvision: {
38
42
  async run(target, options) {
39
43
  return provisionRunwayMobile(target, options);
40
- }
44
+ },
45
+ before(target, { json, platform, resolveOnly }) {
46
+ writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
47
+ if (!resolveOnly) clearMobileReleaseArtifactState(target);
48
+ },
49
+ launchNext: true
41
50
  },
42
51
  devServer: {
43
52
  label: "metro",
@@ -72,6 +81,24 @@ const mobileSurface = {
72
81
  return runDeviceLaunch(context, mobileLaunchProfile);
73
82
  },
74
83
  actions: mobileActions,
84
+ harness: mobileHarness,
85
+ runtimeContext: mobileRuntimeContext,
86
+ readiness: mobileReadiness,
87
+ fixtures: {
88
+ operations: ["init", "sync", "set", "reset"],
89
+ apply: async (context) => (await import("./fixtures.js")).applyMobileFixture(context)
90
+ },
91
+ prepare: mobilePrepare,
92
+ debug: {
93
+ refuse: ({ worker }) => worker ? { message: "--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.", userAction: "mm-harness debug --dev-menu" } : void 0,
94
+ args: ({ devMenu }) => [
95
+ ...process.env.WATCHER_PORT ? ["--port", process.env.WATCHER_PORT] : [],
96
+ "--action",
97
+ devMenu ? "dev-menu" : "debug"
98
+ ]
99
+ },
100
+ reload: async (target, json) => (await import("./reload.js")).reloadMobile(target, json),
101
+ runtimeDecision: mobileRuntimeDecision,
75
102
  run: mobileRun,
76
103
  recording: mobileRecording,
77
104
  diagnostics: mobileDiagnostics,
@@ -0,0 +1,22 @@
1
+ import { terminalAppPort, terminalCdpPort } from "./surface.js";
2
+ async function terminalDoctorChecks(target) {
3
+ const { terminalReadiness } = await import("../../../adapters/terminal/lib/readiness.mjs");
4
+ const { resolveBrowser } = await import("../../../adapters/terminal/launch.mjs");
5
+ const report = await terminalReadiness({
6
+ target,
7
+ appPort: terminalAppPort(),
8
+ cdpPort: terminalCdpPort(),
9
+ signer: process.env.TERMINAL_SIGNER || "extension",
10
+ account: process.env.TERMINAL_ACCOUNT || "dev1",
11
+ probeBrowser: resolveBrowser
12
+ });
13
+ return report.checks.map((check) => ({
14
+ id: `terminal-${check.id}`,
15
+ status: check.status === "pass" ? "pass" : "fail",
16
+ required: check.required,
17
+ message: check.detail
18
+ }));
19
+ }
20
+ export {
21
+ terminalDoctorChecks
22
+ };
@@ -0,0 +1,18 @@
1
+ import path from "node:path";
2
+ import { runnerDir } from "../../paths.js";
3
+ const terminalHarness = {
4
+ install: { entry: "adapters/terminal/inject.mjs", fallback: "adapters/terminal/inject.mjs", node: true },
5
+ cleanup: { entry: "adapters/terminal/cleanup.mjs", fallback: "adapters/terminal/cleanup.mjs", node: true },
6
+ verify: () => ({ command: process.execPath, prefixArgs: [path.join(runnerDir, "adapters", "terminal", "verify.mjs")] }),
7
+ restart: "mm-harness launch"
8
+ };
9
+ const terminalRuntimeContext = {
10
+ forbiddenFields: ["metroPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"],
11
+ resources: ({ existing, defaults, envPort }) => ({
12
+ cdpPort: envPort("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort
13
+ })
14
+ };
15
+ export {
16
+ terminalHarness,
17
+ terminalRuntimeContext
18
+ };
@@ -0,0 +1,11 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ const terminalReadiness = {
4
+ compatibilityMode: (target) => fs.existsSync(path.join(target, "src/features/perpetuals")) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge",
5
+ liveChecks: async (target) => (await import("./doctor-checks.js")).terminalDoctorChecks(target),
6
+ // Screenshots come from the slot browser over CDP.
7
+ captureProviders: ["cdp"]
8
+ };
9
+ export {
10
+ terminalReadiness
11
+ };
@@ -5,6 +5,8 @@ import { TERMINAL_CONSOLE_FILES, terminalRuntimeFile } from "./console-capture.j
5
5
  import { stopTerminalBrowserSync } from "./stop.js";
6
6
  import { adapterRegistry } from "../registry.js";
7
7
  import { terminalActions } from "./actions.js";
8
+ import { terminalHarness, terminalRuntimeContext } from "./overlay.js";
9
+ import { terminalReadiness } from "./readiness.js";
8
10
  import { terminalDiagnostics, terminalObservation } from "./run.js";
9
11
  function portFromEnv(...names) {
10
12
  for (const name of names) {
@@ -33,8 +35,8 @@ const terminalSurface = {
33
35
  resolveSlotPorts() {
34
36
  },
35
37
  async runtimeStatus(target) {
36
- const { terminalReadiness } = await import("../../../adapters/terminal/lib/readiness.mjs");
37
- const report = await terminalReadiness({
38
+ const { terminalReadiness: terminalReadiness2 } = await import("../../../adapters/terminal/lib/readiness.mjs");
39
+ const report = await terminalReadiness2({
38
40
  target: path.resolve(target),
39
41
  appPort: terminalAppPort(),
40
42
  cdpPort: terminalCdpPort(),
@@ -101,6 +103,17 @@ const terminalSurface = {
101
103
  },
102
104
  launch: async (context) => (await import("./launch.js")).launchTerminal(context),
103
105
  actions: terminalActions,
106
+ harness: terminalHarness,
107
+ runtimeContext: terminalRuntimeContext,
108
+ readiness: terminalReadiness,
109
+ fixtures: { operations: ["init", "sync", "set"] },
110
+ debug: {
111
+ args: ({ devMenu }) => [
112
+ ...process.env.WATCHER_PORT ? ["--port", process.env.WATCHER_PORT] : [],
113
+ "--action",
114
+ devMenu ? "dev-menu" : "debug"
115
+ ]
116
+ },
104
117
  diagnostics: terminalDiagnostics,
105
118
  observation: terminalObservation
106
119
  };
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { adapterRegistry } from "./adapters/registry.js";
1
2
  import { detectAdapter, handleHarness } from "./harness.js";
2
3
  import { handleActions } from "./commands/manifest.js";
3
4
  import { handleDoctor } from "./commands/doctor.js";
@@ -108,7 +109,8 @@ async function main(argv) {
108
109
  const { options } = parseArgs(argv.slice(1), command);
109
110
  const target = targetPath(options);
110
111
  const adapterRaw = (typeof options["adapter"] === "string" ? options["adapter"] : void 0) ?? (typeof options["platform"] === "string" ? options["platform"] : void 0) ?? detectAdapter(target);
111
- if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core" || adapterRaw === "terminal") {
112
+ await import("./adapters/surface.js");
113
+ if (adapterRaw && adapterRegistry.has(adapterRaw)) {
112
114
  await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
113
115
  }
114
116
  } catch {
@@ -10,36 +10,17 @@ async function handleDebug(argv) {
10
10
  const adapter = resolveAdapter(options, target);
11
11
  if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
12
12
  const surface = getAdapterSurface(adapter);
13
- if (surface.headless) return usageOut(json, "debug", "core is headless; there is no debug console.", surface.hints.relaunch);
13
+ if (surface.headless) return usageOut(json, "debug", `${adapter} is headless; there is no debug console.`, surface.hints.relaunch);
14
14
  surface.resolveSlotPorts(target);
15
15
  const worker = flag(options, "worker");
16
16
  const devMenu = flag(options, "devMenu");
17
17
  const noOpen = flag(options, "noOpen");
18
- if (adapter === "mobile" && worker) {
19
- return usageOut(
20
- json,
21
- "debug",
22
- "--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.",
23
- "mm-harness debug --dev-menu"
24
- );
25
- }
26
- if (adapter === "extension" && devMenu) {
27
- return usageOut(
28
- json,
29
- "debug",
30
- "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.",
31
- "mm-harness debug --worker"
32
- );
33
- }
18
+ const debug = surface.debug;
19
+ if (!debug) return usageOut(json, "debug", `${adapter} has no debug console.`, surface.hints.relaunch);
20
+ const refused = debug.refuse?.({ worker, devMenu });
21
+ if (refused) return usageOut(json, "debug", refused.message, refused.userAction);
34
22
  const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
35
- const debugArgs = ["--adapter", adapter];
36
- if (adapter === "extension") {
37
- if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
38
- debugArgs.push("--target", worker ? "worker" : "page");
39
- } else {
40
- if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
41
- debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
42
- }
23
+ const debugArgs = ["--adapter", adapter, ...debug.args({ worker, devMenu })];
43
24
  if (json) debugArgs.push("--json");
44
25
  if (noOpen) debugArgs.push("--no-open");
45
26
  const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
@@ -4,6 +4,7 @@ import {
4
4
  findAvailableIosSimulatorById,
5
5
  listConnectedDevices
6
6
  } from "../devices.js";
7
+ import { getAdapterSurface } from "../adapters/surface.js";
7
8
  import { persistExplicitDevicePin } from "../runtime-context.js";
8
9
  import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
9
10
  const DEVICE_SELECT_HINT = "Pass --device <name|udid|serial> to select one target. Do not shut down other connected devices to force selection.";
@@ -97,7 +98,7 @@ function deviceRecovery(options, rerun) {
97
98
  function applyDeviceTargeting(command, adapter, options, opts) {
98
99
  const device = optionString(options, "device");
99
100
  const platformPreference = mobilePlatformPreference(options);
100
- if (adapter !== "mobile") {
101
+ if (!getAdapterSurface(adapter).readiness.devices) {
101
102
  if (device !== void 0) {
102
103
  return {
103
104
  ok: false,