@deeeed/metamask-harness 0.75.0 → 0.77.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 (49) hide show
  1. package/CHANGELOG.md +28 -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/call.js +3 -7
  27. package/dist/commands/debug.js +6 -25
  28. package/dist/commands/device-target.js +2 -1
  29. package/dist/commands/doctor.js +26 -137
  30. package/dist/commands/fixtures.js +13 -217
  31. package/dist/commands/list-executables.js +12 -22
  32. package/dist/commands/logs.js +1 -1
  33. package/dist/commands/manifest.js +11 -133
  34. package/dist/commands/parse-args.js +9 -4
  35. package/dist/commands/prepare.js +23 -53
  36. package/dist/commands/provision.js +3 -7
  37. package/dist/commands/reload.js +8 -47
  38. package/dist/commands/run.js +2 -5
  39. package/dist/commands/runtime-decision.js +5 -27
  40. package/dist/commands/self-test.js +1 -3
  41. package/dist/commands/status.js +20 -28
  42. package/dist/commands/stop.js +1 -1
  43. package/dist/doctor.js +9 -119
  44. package/dist/harness.js +31 -100
  45. package/dist/mm-harness-cli.js +3 -2
  46. package/dist/paths.js +8 -2
  47. package/dist/runtime-context.js +12 -17
  48. package/docs/CONTRIBUTING.md +27 -17
  49. package/package.json +2 -2
@@ -1,28 +1,16 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
- import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
6
4
  import { featureFlagReport } from "../feature-flags.js";
7
5
  import { color } from "../cli-color.js";
8
6
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
9
7
  import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
10
- import { terminalAppPort, terminalCdpPort } from "../adapters/terminal/surface.js";
11
8
  import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
12
9
  import { assertAdapter, runnerDir } from "../paths.js";
13
10
  import { getAdapterSurface } from "../adapters/surface.js";
14
- import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
15
- import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
16
11
  import { loadActionManifest, validateManifest } from "../manifest.js";
17
12
  import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
18
13
  import { applyDeviceTargeting } from "./device-target.js";
19
- import {
20
- mobileDeviceView,
21
- mobileDeviceLiveView,
22
- renderAdditionalReachableDevices,
23
- renderMobileDeviceList,
24
- renderMobileLiveBlock
25
- } from "./mobile-device-view.js";
26
14
  import { farmslotReadyIndicator } from "./farmslot-ready.js";
27
15
  import { updateStatus } from "./update.js";
28
16
  import { ensureRuntimeContext } from "../runtime-context.js";
@@ -63,6 +51,7 @@ async function handleDoctor({ options }) {
63
51
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
64
52
  }
65
53
  assertAdapter(adapter);
54
+ const { readiness } = getAdapterSurface(adapter);
66
55
  if (!fs.existsSync(target)) {
67
56
  return usageOut(
68
57
  json,
@@ -110,10 +99,9 @@ async function handleDoctor({ options }) {
110
99
  runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
111
100
  }
112
101
  const ready = runtime2?.decision === "ready";
113
- if (adapter === "extension" && runtime2?.reasonCode === "product-config-required") {
114
- failed.push("extension-product-config");
115
- }
116
- const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
102
+ const block = readiness.runtimeBlock?.(target, runtime2);
103
+ if (block) failed.push(block.id);
104
+ const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target, block);
117
105
  if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
118
106
  const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
119
107
  const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
@@ -136,13 +124,12 @@ async function handleDoctor({ options }) {
136
124
  }
137
125
  return status === "pass" ? 0 : 1;
138
126
  }
139
- const mobilePlatform = adapter === "mobile" && (platformOption === "ios" || platformOption === "android") ? platformOption : void 0;
140
127
  const result = createDoctorReport(
141
128
  adapter,
142
129
  target,
143
130
  manifestValidation,
144
131
  actionManifestPath,
145
- mobilePlatform
132
+ platformOption
146
133
  );
147
134
  const view = collectCheckoutView(target, adapter);
148
135
  const missingRequiredTool = result.checks.find(
@@ -183,7 +170,7 @@ async function handleDoctor({ options }) {
183
170
  const checks = [
184
171
  ...result.checks,
185
172
  runtimeCheck,
186
- ...adapter === "terminal" ? await terminalDoctorChecks(target) : []
173
+ ...await readiness.liveChecks?.(target) ?? []
187
174
  ];
188
175
  const requiredChecks = requiredDoctorCheckSummary(checks);
189
176
  const doctorResult = {
@@ -192,10 +179,11 @@ async function handleDoctor({ options }) {
192
179
  checks,
193
180
  requiredChecks
194
181
  };
195
- const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
196
- const capture = captureHealth(adapter);
197
- const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
198
- const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
182
+ const orphanMetros = readiness.orphanDevServers?.(target, process.env.WATCHER_PORT) ?? [];
183
+ const capture = captureHealth(readiness.captureProviders);
184
+ const deviceSource = await readiness.devices?.();
185
+ const deviceView = deviceSource ? deviceSource.view(allDevices) : null;
186
+ const liveView = deviceSource && deviceView ? await deviceSource.live(target, deviceView) : null;
199
187
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
200
188
  const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
201
189
  const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
@@ -216,8 +204,8 @@ async function handleDoctor({ options }) {
216
204
  }
217
205
  const skills = skillsView(target);
218
206
  const update = updateStatus();
219
- const extensionFlags = adapter === "extension" ? readExtensionPinnedFlags(target) : null;
220
- const featureFlags = extensionFlags ? featureFlagReport(extensionFlags.overrides, extensionFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
207
+ const pinnedFlags = readiness.pinnedFlags?.(target) ?? null;
208
+ const featureFlags = pinnedFlags ? featureFlagReport(pinnedFlags.overrides, pinnedFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
221
209
  const doctorUserAction = doctorResult.checks.find(
222
210
  (check) => check.required && check.status === "fail" && check.userAction
223
211
  )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
@@ -249,12 +237,7 @@ async function handleDoctor({ options }) {
249
237
  featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
250
238
  featureFlags.summary.replace(/^feature flags: /u, "")
251
239
  )}`);
252
- if (adapter === "mobile") {
253
- const perps = mobilePerpsEnvironment(target);
254
- console.log(
255
- `mobile-perps: ${perps.status} (${perps.file}; MM_PERPS_ENABLED=${perps.perpsEnabled.value ?? "missing"}; OVERRIDE_REMOTE_FEATURE_FLAGS=${perps.remoteFeatureFlagOverride.value ?? "missing"})`
256
- );
257
- }
240
+ for (const line of readiness.lines?.(target) ?? []) console.log(line);
258
241
  if (orphanMetros.length > 0) {
259
242
  console.log(
260
243
  `${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
@@ -264,33 +247,15 @@ async function handleDoctor({ options }) {
264
247
  if (capture) renderCaptureHealth(capture, out);
265
248
  renderSkillsView(skills, out);
266
249
  renderUpdateStatus(update, out);
267
- if (adapter === "mobile") {
268
- if (deviceView) renderMobileDeviceList(deviceView, out);
269
- if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
270
- if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
250
+ if (deviceSource) {
251
+ if (deviceView) deviceSource.renderList(deviceView, out);
252
+ if (additionalReachableDevices.length > 0) deviceSource.renderAdditional(additionalReachableDevices, out);
253
+ if (liveView) deviceSource.renderLive(deviceView?.devices ?? [], liveView.liveMap, out);
271
254
  }
272
255
  if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
273
256
  }
274
257
  return doctorResult.status === "pass" ? 0 : 1;
275
258
  }
276
- async function terminalDoctorChecks(target) {
277
- const { terminalReadiness } = await import("../../adapters/terminal/lib/readiness.mjs");
278
- const { resolveBrowser } = await import("../../adapters/terminal/launch.mjs");
279
- const report = await terminalReadiness({
280
- target,
281
- appPort: terminalAppPort(),
282
- cdpPort: terminalCdpPort(),
283
- signer: process.env.TERMINAL_SIGNER || "extension",
284
- account: process.env.TERMINAL_ACCOUNT || "dev1",
285
- probeBrowser: resolveBrowser
286
- });
287
- return report.checks.map((check) => ({
288
- id: `terminal-${check.id}`,
289
- status: check.status === "pass" ? "pass" : "fail",
290
- required: check.required,
291
- message: check.detail
292
- }));
293
- }
294
259
  function doctorRuntimeCheck(runtime, error, expectLive) {
295
260
  if (error) {
296
261
  return {
@@ -310,7 +275,7 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
310
275
  detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
311
276
  };
312
277
  }
313
- function doctorFixNextActions(failed, fixture, adapter, target) {
278
+ function doctorFixNextActions(failed, fixture, adapter, target, block) {
314
279
  const actions = [];
315
280
  const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
316
281
  const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
@@ -325,10 +290,7 @@ function doctorFixNextActions(failed, fixture, adapter, target) {
325
290
  );
326
291
  }
327
292
  if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
328
- if (adapter === "extension" && failed.includes("extension-product-config")) {
329
- const block = extensionProductConfigBlock(target);
330
- if (block) actions.push(block.userAction);
331
- }
293
+ if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
332
294
  return actions;
333
295
  }
334
296
  function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
@@ -367,9 +329,9 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
367
329
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
368
330
  return EXIT.runtime;
369
331
  }
370
- function captureHealth(adapter) {
371
- if (adapter === "core") return null;
372
- const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
332
+ function captureHealth(providers) {
333
+ if (!providers) return null;
334
+ const fallback = [...providers];
373
335
  const helper = captureHelperHealth();
374
336
  if (!helper) {
375
337
  return {
@@ -445,18 +407,6 @@ function renderUpdateStatus(status, out) {
445
407
  status.updateAvailable ? `${out("label", "update:")} ${out("warn", `${status.current} \u2192 ${status.latest} available`)} ${out("dim", "(mm-harness update)")}` : status.latest && status.latest !== status.current ? `${out("label", "update:")} ${out("ok", status.current)} ${out("dim", `ahead of registry ${status.latest}`)}` : `${out("label", "update:")} ${out("ok", `${status.current} is latest`)}`
446
408
  );
447
409
  }
448
- function detectOrphanMetros(target, livePort) {
449
- try {
450
- const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
451
- const out = execFileSync("bash", ["-c", `. "$1"; detect_checkout_metros "$2" "$3"`, "mm-harness-doctor", script, target, livePort ?? ""], {
452
- encoding: "utf8",
453
- timeout: 5e3
454
- });
455
- return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
456
- } catch {
457
- return [];
458
- }
459
- }
460
410
  async function runDoctorFix(adapter, target, manifestValidation, json) {
461
411
  const fixed = [];
462
412
  const failed = [];
@@ -473,18 +423,11 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
473
423
  } catch {
474
424
  failed.push("runtime-context");
475
425
  }
476
- if (adapter === "extension") {
477
- try {
478
- if (ensureExtensionPerpsBuildFlag(target)) fixed.push("extension-perps-build");
479
- } catch {
480
- failed.push("extension-perps-build");
481
- }
482
- }
483
- if (adapter === "mobile") {
426
+ for (const fix of getAdapterSurface(adapter).readiness.fixes ?? []) {
484
427
  try {
485
- if (ensureMobilePerpsEnvironment(target).length > 0) fixed.push("mobile-perps-env");
428
+ if (fix.apply(target)) fixed.push(fix.id);
486
429
  } catch {
487
- failed.push("mobile-perps-env");
430
+ failed.push(fix.id);
488
431
  }
489
432
  }
490
433
  const state = newHealState();
@@ -508,62 +451,8 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
508
451
  }
509
452
  return { fixed, failed };
510
453
  }
511
- function ensureExtensionPerpsBuildFlag(target) {
512
- const configPath = path.join(target, ".metamaskrc");
513
- const templatePath = path.join(target, ".metamaskrc.dist");
514
- const config = readTextSnapshot(configPath);
515
- const template = config.kind === "file" ? null : readTextSnapshot(templatePath);
516
- const source = config.kind === "file" ? config : template?.kind === "file" ? template : config.kind === "symlink" ? { kind: "file", text: "", mode: 438 } : null;
517
- if (!source) return false;
518
- const before = source.text;
519
- if (/^\s*PERPS_ENABLED\s*=\s*(['"]?)true\1\s*$/mu.test(before)) return false;
520
- const line = "PERPS_ENABLED='true'";
521
- const after = /^\s*PERPS_ENABLED\s*=.*$/mu.test(before) ? before.replace(/^\s*PERPS_ENABLED\s*=.*$/mu, line) : `${before.replace(/\s*$/u, "")}
522
- ${line}
523
- `;
524
- const temporary = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
525
- let descriptor = null;
526
- try {
527
- const mode = config.kind === "file" ? config.mode : 438;
528
- descriptor = fs.openSync(
529
- temporary,
530
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
531
- mode
532
- );
533
- fs.writeFileSync(descriptor, after);
534
- if (config.kind === "file") fs.fchmodSync(descriptor, config.mode);
535
- fs.closeSync(descriptor);
536
- descriptor = null;
537
- fs.renameSync(temporary, configPath);
538
- } finally {
539
- if (descriptor !== null) fs.closeSync(descriptor);
540
- fs.rmSync(temporary, { force: true });
541
- }
542
- return true;
543
- }
544
- function readTextSnapshot(file) {
545
- let descriptor;
546
- try {
547
- descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
548
- } catch (error) {
549
- const code = error.code;
550
- if (code === "ENOENT") return { kind: "missing" };
551
- if (code === "ELOOP") return { kind: "symlink" };
552
- throw error;
553
- }
554
- try {
555
- return {
556
- kind: "file",
557
- text: fs.readFileSync(descriptor, "utf8"),
558
- mode: fs.fstatSync(descriptor).mode & 511
559
- };
560
- } finally {
561
- fs.closeSync(descriptor);
562
- }
563
- }
564
454
  export {
565
455
  doctorFixNextActions,
566
- ensureExtensionPerpsBuildFlag,
567
456
  handleDoctor,
568
457
  skillsView
569
458
  };
@@ -5,15 +5,11 @@ import { runnerDir, walletFixturePath } from "../paths.js";
5
5
  import { getAdapterSurface } from "../adapters/surface.js";
6
6
  import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
7
7
  import { acquireCheckoutLock } from "../checkout-lock.js";
8
- import { colorHumanMessage } from "../cli-color.js";
9
8
  import { applyDeviceTargeting } from "./device-target.js";
10
9
  import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
11
- import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec, sameFeatureFlags } from "../feature-flags.js";
12
- import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
10
+ import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec } from "../feature-flags.js";
13
11
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
14
12
  const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
15
- const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
16
- const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
17
13
  async function handleFixtures(argv) {
18
14
  const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
19
15
  if (!positional[0]) return fixturesStatus(options);
@@ -52,7 +48,7 @@ function fixturesStatus(options) {
52
48
  const fixture = { ...summary, path: canonical };
53
49
  const ready = summary.status === "ready";
54
50
  const nextCommand = ready ? "mm-harness fixtures set" : "mm-harness fixtures init --from <path> # or: mm-harness fixtures init --dev";
55
- const operations = adapter === "extension" ? ["init", "sync", "set", "reset", "generate", "finalize"] : adapter === "mobile" ? ["init", "sync", "set", "reset"] : ["init", "sync", "set"];
51
+ const operations = [...getAdapterSurface(adapter).fixtures.operations];
56
52
  if (json) {
57
53
  console.log(JSON.stringify({
58
54
  schemaVersion: 1,
@@ -191,217 +187,16 @@ async function handleFixturesLocked(argv) {
191
187
  if (!(error instanceof FeatureFlagSpecError)) throw error;
192
188
  return usageOut(json, "fixtures", error.message, error.userAction);
193
189
  }
194
- const fixtureFlagsJson = JSON.stringify(fixtureFlags);
195
- if (adapter === "extension") {
196
- return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub, fixtureFlags);
197
- }
198
- const reset = sub === "reset";
199
- 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
200
- `);
201
- let status;
202
- let mobileSetupResult;
203
- let resetError;
204
- if (adapter === "mobile") {
205
- const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
206
- const previousAppRoot = process.env.APP_ROOT;
207
- process.env.APP_ROOT = target;
208
- try {
209
- if (reset) {
210
- const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
211
- const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
212
- const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
213
- const resetArgs = ["--platform", platform, "--target", target];
214
- if (platform === "ios") {
215
- resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
216
- } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
217
- resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
218
- }
219
- const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
220
- timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
221
- });
222
- if (resetResult.status !== 0) {
223
- resetError = new Error("Mobile app data reset failed.");
224
- } else {
225
- const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
226
- const prewarmResult = await spawnScriptStreaming(
227
- prewarmBundle,
228
- ["--platform", platform, "--target", target, "--port", port],
229
- target,
230
- {
231
- timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
232
- }
233
- );
234
- if (prewarmResult.status !== 0) {
235
- resetError = new Error("Mobile bundle prewarm failed after app data reset.");
236
- }
237
- }
238
- if (!resetError) {
239
- const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
240
- const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
241
- if (platform === "ios") {
242
- openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
243
- } else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
244
- openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
245
- }
246
- const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
247
- timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
248
- });
249
- if (openResult.status !== 0) {
250
- resetError = new Error("Mobile app relaunch after data reset failed.");
251
- } else {
252
- const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
253
- const waitResult = await spawnScriptStreaming(
254
- waitForBridge,
255
- ["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
256
- target,
257
- { timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
258
- );
259
- if (waitResult.status !== 0) {
260
- resetError = new Error("Mobile bridge did not return after app data reset.");
261
- }
262
- }
263
- }
264
- }
265
- const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_SETUP_WALLET_TIMEOUT_MS);
266
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_SETUP_WALLET_TIMEOUT_MS;
267
- if (!resetError) {
268
- mobileSetupResult = await spawnScriptStreaming(
269
- setupWalletSh,
270
- ["--fixture", fixturePath, "--remote-flags-json", fixtureFlagsJson],
271
- target,
272
- {
273
- env: {
274
- APP_ROOT: target,
275
- WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
276
- METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
277
- },
278
- timeoutMs
279
- }
280
- );
281
- }
282
- status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
283
- } finally {
284
- if (previousAppRoot === void 0) delete process.env.APP_ROOT;
285
- else process.env.APP_ROOT = previousAppRoot;
286
- }
287
- if (!json && status === "fail") {
288
- console.error(` Next: ${retryHint}`);
289
- }
290
- }
291
- const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
292
- if (json) {
293
- console.log(
294
- JSON.stringify(
295
- {
296
- schemaVersion: 1,
297
- command: "fixtures",
298
- action: sub,
299
- adapter,
300
- fixture: fixturePath,
301
- canonicalFixture,
302
- status,
303
- exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
304
- walletReset: reset && status === "pass",
305
- message: teaching,
306
- error: status === "fail" ? {
307
- code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
308
- 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"}`,
309
- userAction: retryHint
310
- } : null
311
- },
312
- null,
313
- 2
314
- )
190
+ const apply = surface.fixtures.apply;
191
+ if (!apply) {
192
+ return usageOut(
193
+ json,
194
+ "fixtures",
195
+ `fixtures ${sub} has no wallet to apply on ${adapter}.`,
196
+ surface.hints.launch
315
197
  );
316
- } else if (status === "pass") {
317
- console.error(teaching);
318
- }
319
- return status === "pass" ? EXIT.ok : EXIT.runtime;
320
- }
321
- async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action, fixtureFlags) {
322
- const distManifest = path.join(target, "dist/chrome/manifest.json");
323
- const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
324
- if (!fs.existsSync(distManifest)) {
325
- const message2 = `Extension fixture needs an existing dist/chrome build; no manifest was found at ${distManifest}.`;
326
- if (json) {
327
- console.log(JSON.stringify({
328
- schemaVersion: 1,
329
- command: "fixtures",
330
- action,
331
- adapter: "extension",
332
- fixture: fixturePath,
333
- canonicalFixture,
334
- status: "fail",
335
- exitCode: EXIT.runtime,
336
- error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
337
- }, null, 2));
338
- } else {
339
- console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
340
- Next: ${userAction}`);
341
- }
342
- return EXIT.runtime;
343
198
  }
344
- process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
345
- `);
346
- const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
347
- const args = ["--target", target, "--launch-existing-dist", "--launch-only", "--reset-profile"];
348
- const flagKeys = Object.keys(fixtureFlags);
349
- if (flagKeys.length > 0) args.push("--remote-flags-json", JSON.stringify(fixtureFlags));
350
- if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
351
- const result = await spawnScriptStreaming(liveScript, args, target, {
352
- env: { RECIPE_WALLET_FIXTURE: fixturePath },
353
- timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
354
- });
355
- const pinned = readExtensionPinnedFlags(target);
356
- const pinnedFlags = pinned.pinned ?? {};
357
- const flagMismatch = result.status === 0 && !pinned.error && (pinned.pinned === null || !sameFeatureFlags(pinnedFlags, fixtureFlags)) ? [.../* @__PURE__ */ new Set([...flagKeys, ...Object.keys(pinnedFlags)])].filter(
358
- (key) => JSON.stringify(pinnedFlags[key]) !== JSON.stringify(fixtureFlags[key])
359
- ) : [];
360
- if (result.status === 0 && (pinned.error || flagMismatch.length > 0)) {
361
- const detail = pinned.error ? `the launched snapshot manifest could not be read (${pinned.sourcePath ?? "unknown path"}): ${pinned.error}` : `snapshot pin disagrees with the fixture for: ${flagMismatch.join(", ")}`;
362
- const flagNext = `mm-harness launch --adapter extension --build --target ${JSON.stringify(target)}`;
363
- if (json) {
364
- console.log(JSON.stringify({
365
- schemaVersion: 1,
366
- command: "fixtures",
367
- action,
368
- adapter: "extension",
369
- fixture: fixturePath,
370
- canonicalFixture,
371
- status: "fail",
372
- exitCode: EXIT.runtime,
373
- error: { code: "EXTENSION_FIXTURE_FLAGS_NOT_APPLIED", message: detail, userAction: flagNext }
374
- }, null, 2));
375
- } else {
376
- console.error(`\u2717 mm-harness fixtures ${action}: ${detail}
377
- Next: ${flagNext}`);
378
- }
379
- return EXIT.runtime;
380
- }
381
- const status = result.status === 0 ? "pass" : "fail";
382
- const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
383
- const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
384
- if (json) {
385
- console.log(JSON.stringify({
386
- schemaVersion: 1,
387
- command: "fixtures",
388
- action,
389
- adapter: "extension",
390
- fixture: fixturePath,
391
- canonicalFixture,
392
- status,
393
- exitCode,
394
- profileReset: status === "pass",
395
- message,
396
- error: status === "fail" ? { code: "EXTENSION_FIXTURE_APPLY_FAILED", message, userAction } : null
397
- }, null, 2));
398
- } else if (status === "pass") {
399
- console.error(message);
400
- } else {
401
- console.error(`\u2717 mm-harness fixtures ${action}: ${message}
402
- Next: ${userAction}`);
403
- }
404
- return exitCode;
199
+ return apply({ target, sub, fixturePath, canonicalFixture, fixtureFlags, json, retryHint });
405
200
  }
406
201
  const EXT_FIXTURE_LEAF = "adapters/extension/wallet-fixture-state.cjs";
407
202
  function emitLeafMissing(json, action, fields, leaf) {
@@ -422,7 +217,7 @@ function emitLeafMissing(json, action, fields, leaf) {
422
217
  return EXIT.runtime;
423
218
  }
424
219
  function fixturesGenerate(adapter, target, options, json) {
425
- if (adapter !== "extension") {
220
+ if (!getAdapterSurface(adapter).fixtures.operations.includes("generate")) {
426
221
  return usageOut(
427
222
  json,
428
223
  "fixtures",
@@ -486,7 +281,7 @@ function fixturesGenerate(adapter, target, options, json) {
486
281
  return exitCode;
487
282
  }
488
283
  async function fixturesFinalize(adapter, target, options, json) {
489
- if (adapter !== "extension") {
284
+ if (!getAdapterSurface(adapter).fixtures.operations.includes("finalize")) {
490
285
  return usageOut(
491
286
  json,
492
287
  "fixtures",
@@ -678,5 +473,6 @@ Wallet fixture: ${canonical}`);
678
473
  return EXIT.ok;
679
474
  }
680
475
  export {
476
+ FIXTURE_STREAM_TIMEOUT_MS,
681
477
  handleFixtures
682
478
  };
@@ -6,12 +6,12 @@ import { color } from "../cli-color.js";
6
6
  import { EXIT } from "./shared.js";
7
7
  import { optionFlag, optionString, optionStrings, resolveAdapter, shellQuoteArg } from "./parse-args.js";
8
8
  import { actionDomain, describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
9
+ import { shortActionNames } from "@farmslot/recipe-cli";
9
10
  import {
10
11
  describeRunnableRecipe,
11
12
  listRunnableRecipes,
12
13
  resolveMetaMaskLibrarySources
13
14
  } from "./run-engine.js";
14
- const finalSegment = (name) => name.split(".").pop() ?? name;
15
15
  const recipeDomain = (name) => name.includes(".") ? name.split(".")[0].replaceAll("_", "-") : "general";
16
16
  function catalogDescription(description) {
17
17
  if (!description) return "";
@@ -91,27 +91,17 @@ ${out(sort === "domain" ? domainStyles.get(group) : "bold", label)} ${out("dim",
91
91
  const protocolNames = new Set(getRecipeActionManifestActionNames(manifest));
92
92
  const available = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
93
93
  const described = select(available, actionDomain);
94
- const shortCounts = /* @__PURE__ */ new Map();
95
- for (const entry of available) {
96
- const short = finalSegment(entry.name);
97
- shortCounts.set(short, (shortCounts.get(short) ?? 0) + 1);
98
- }
99
- const shortByName = /* @__PURE__ */ new Map();
100
- const actions = described.map((entry) => {
101
- const short = finalSegment(entry.name);
102
- const usableShort = shortCounts.get(short) === 1 ? short : null;
103
- shortByName.set(entry.name, usableShort);
104
- return {
105
- name: entry.name,
106
- short: usableShort,
107
- description: entry.description,
108
- fields: entry.fields,
109
- source: entry.source,
110
- sourceTier: entry.sourceTier,
111
- sourceManifest: entry.sourceManifest,
112
- shadows: entry.shadows
113
- };
114
- });
94
+ const shortByName = shortActionNames(available.map((entry) => entry.name));
95
+ const actions = described.map((entry) => ({
96
+ name: entry.name,
97
+ short: shortByName.get(entry.name) ?? null,
98
+ description: entry.description,
99
+ fields: entry.fields,
100
+ source: entry.source,
101
+ sourceTier: entry.sourceTier,
102
+ sourceManifest: entry.sourceManifest,
103
+ shadows: entry.shadows
104
+ }));
115
105
  if (json) {
116
106
  console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
117
107
  return EXIT.ok;
@@ -68,7 +68,7 @@ async function handleLogs(argv) {
68
68
  if (flag(options, "window")) {
69
69
  surface.resolveSlotPorts(target);
70
70
  const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "default";
71
- const windowName = adapter === "mobile" ? `metro-${port}` : `webpack-${port}`;
71
+ const windowName = `${surface.devServer.label}-${port}`;
72
72
  const devLog = logSources[0].path;
73
73
  const leaf = path.join(runnerDir, "adapters/shared/open-log-window.sh");
74
74
  const result = spawnScript(