@deeeed/metamask-harness 0.5.1 → 0.6.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 (89) hide show
  1. package/CHANGELOG.md +37 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +2 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/sidepanel-toggle.sh +33 -12
  8. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  9. package/adapters/manifest.json +13 -0
  10. package/adapters/mobile/inject.sh +1 -0
  11. package/adapters/mobile/stop-metro.sh +20 -0
  12. package/adapters/shared/cli-ux.sh +20 -2
  13. package/adapters/shared/ensure-runner-deps.sh +30 -0
  14. package/adapters/shared/reap-checkout-metros.sh +53 -0
  15. package/adapters/shared/recipe-harness-root.mjs +23 -0
  16. package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
  17. package/bin/mm-harness +45 -4
  18. package/dist/adapters/extension/runtime.js +3 -1
  19. package/dist/adapters/mobile/provision.js +34 -1
  20. package/dist/adapters/slot-ports.js +3 -8
  21. package/dist/cli.js +32 -1463
  22. package/dist/commands/call.js +183 -0
  23. package/dist/commands/completion-candidates.js +58 -0
  24. package/dist/commands/doctor.js +101 -0
  25. package/dist/commands/ensure-ready.js +24 -0
  26. package/dist/commands/flows.js +62 -0
  27. package/dist/commands/launch/extension.js +40 -0
  28. package/dist/commands/{launch.js → launch/index.js} +15 -47
  29. package/dist/commands/launch/mobile.js +10 -0
  30. package/dist/commands/manifest.js +72 -0
  31. package/dist/commands/parse-args.js +189 -0
  32. package/dist/commands/provision.js +136 -0
  33. package/dist/commands/resolve-extension.js +23 -0
  34. package/dist/commands/run-engine.js +341 -0
  35. package/dist/commands/run.js +217 -0
  36. package/dist/commands/runtime-decision.js +58 -0
  37. package/dist/commands/runtime-health.js +25 -0
  38. package/dist/commands/runtime-launch.js +139 -0
  39. package/dist/commands/self-test.js +52 -0
  40. package/dist/commands/stop.js +52 -0
  41. package/dist/harness.js +8 -48
  42. package/dist/mm-harness-cli.js +13 -8
  43. package/docs/CLI-SPEC.md +1 -1
  44. package/docs/CODE-MAP.md +62 -0
  45. package/library/README.md +14 -0
  46. package/library/actions/extension/platform/cdp.mjs +1 -1
  47. package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
  48. package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
  49. package/package.json +6 -7
  50. package/src/adapters/core/surface.ts +0 -71
  51. package/src/adapters/extension/ensure-ready.ts +0 -185
  52. package/src/adapters/extension/extension-id.ts +0 -107
  53. package/src/adapters/extension/runtime-decision.ts +0 -445
  54. package/src/adapters/extension/runtime.ts +0 -407
  55. package/src/adapters/extension/surface.ts +0 -88
  56. package/src/adapters/mobile/deps-markers.ts +0 -21
  57. package/src/adapters/mobile/prepare.ts +0 -246
  58. package/src/adapters/mobile/provision.ts +0 -594
  59. package/src/adapters/mobile/runtime-decision.ts +0 -466
  60. package/src/adapters/mobile/surface.ts +0 -71
  61. package/src/adapters/resolve-farmslot-ports.ts +0 -13
  62. package/src/adapters/slot-ports.ts +0 -158
  63. package/src/adapters/surface.ts +0 -117
  64. package/src/adapters.ts +0 -601
  65. package/src/cli-color.ts +0 -92
  66. package/src/cli-commands.ts +0 -250
  67. package/src/cli-version.ts +0 -141
  68. package/src/cli.ts +0 -2091
  69. package/src/commands/debug.ts +0 -65
  70. package/src/commands/fixtures.ts +0 -198
  71. package/src/commands/launch.ts +0 -470
  72. package/src/commands/logs.ts +0 -99
  73. package/src/commands/shared.ts +0 -235
  74. package/src/commands/update.ts +0 -316
  75. package/src/completions-cache.ts +0 -86
  76. package/src/doctor.ts +0 -215
  77. package/src/harness.ts +0 -797
  78. package/src/heal-bounds.ts +0 -198
  79. package/src/index.ts +0 -15
  80. package/src/leaf-invoke.ts +0 -28
  81. package/src/live-adapter-contract.ts +0 -274
  82. package/src/manifest.ts +0 -47
  83. package/src/mm-harness-cli.ts +0 -655
  84. package/src/paths.ts +0 -198
  85. package/src/progress.ts +0 -117
  86. package/src/recording-target.ts +0 -147
  87. package/src/run-recording.ts +0 -329
  88. package/src/runner.ts +0 -108
  89. package/src/types.ts +0 -57
@@ -51,6 +51,7 @@ function scoreSlot(slot, slotSuffix) {
51
51
  const resources = slot.resources ?? {};
52
52
  const cdp = resources.browser?.cdp_port;
53
53
  const port = resources['dev-server']?.port;
54
+ const simulator = resources['ios-sim']?.simulator;
54
55
  const session = slot.session ?? '';
55
56
  const slotId = slot.id ?? '';
56
57
  let score = 0;
@@ -61,7 +62,7 @@ function scoreSlot(slot, slotSuffix) {
61
62
  if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
62
63
  }
63
64
  if (!slotId.toLowerCase().includes('demo')) score += 5;
64
- return [score, cdp, port, slotId];
65
+ return [score, cdp, port, slotId, simulator];
65
66
  }
66
67
 
67
68
  export function resolveFarmslotPortsByRepo(repo) {
@@ -70,6 +71,12 @@ export function resolveFarmslotPortsByRepo(repo) {
70
71
 
71
72
  const slotSuffix = inferSlotSuffix(realRepo);
72
73
  const roots = [];
74
+ // A slot checkout lives at <workspace>/repos/<slot>; that workspace's own
75
+ // pool is the authoritative one for this repo — machine-global fallbacks
76
+ // (env root, dev checkout) apply only when the repo is not workspace-shaped.
77
+ if (path.basename(path.dirname(realRepo)) === 'repos') {
78
+ roots.push(path.join(path.dirname(path.dirname(realRepo)), 'farmslot'));
79
+ }
73
80
  if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
74
81
  roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
75
82
 
@@ -102,11 +109,12 @@ export function resolveFarmslotPortsByRepo(repo) {
102
109
  }
103
110
 
104
111
  if (best) {
105
- const [, cdp, port, slotId] = best;
112
+ const [, cdp, port, slotId, simulator] = best;
106
113
  return formatKvLines({
107
114
  ...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
108
115
  ...(port !== undefined ? { WATCHER_PORT: port } : {}),
109
116
  ...(slotId ? { SLOT_ID: slotId } : {}),
117
+ ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
110
118
  });
111
119
  }
112
120
  }
@@ -142,9 +150,9 @@ export function resolveMobileRuntimeContext(repo) {
142
150
  if (fs.existsSync(ctxPath)) {
143
151
  try {
144
152
  const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
145
- if (c.simulator || c.metroPort) {
153
+ if (c.simulator || (c.metroPort != null && c.metroPort !== '')) {
146
154
  return formatKvLines({
147
- ...(c.metroPort !== undefined ? { WATCHER_PORT: c.metroPort } : {}),
155
+ ...(c.metroPort != null && c.metroPort !== '' ? { WATCHER_PORT: c.metroPort } : {}),
148
156
  ...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
149
157
  ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
150
158
  });
@@ -166,9 +174,10 @@ function resolveMobileProvisionBaseline(repo) {
166
174
  try {
167
175
  const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
168
176
  const simulator = c.simulator?.name || c.simulator?.udid;
169
- if (!simulator && c.watcherPort === undefined) return null;
177
+ const watcherPort = c.watcherPort != null && c.watcherPort !== '' ? c.watcherPort : undefined;
178
+ if (!simulator && watcherPort === undefined) return null;
170
179
  return formatKvLines({
171
- ...(c.watcherPort !== undefined ? { WATCHER_PORT: c.watcherPort } : {}),
180
+ ...(watcherPort !== undefined ? { WATCHER_PORT: watcherPort } : {}),
172
181
  ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
173
182
  ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
174
183
  });
package/bin/mm-harness CHANGED
@@ -50,15 +50,27 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
50
50
  fi
51
51
  fi
52
52
 
53
+ ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
54
+ ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
55
+
56
+ # Published installs ship dist/ only. Teach before deps work so a broken global
57
+ # install never runs ensure-runner-deps against a partial node_modules tree.
58
+ case "$RUNNER_DIR/" in
59
+ */node_modules/*)
60
+ if [ ! -f "$ENTRY_DIST" ]; then
61
+ echo "mm-harness: installed package is missing dist/mm-harness-cli.js." >&2
62
+ echo " Next: reinstall @deeeed/metamask-harness@latest (npm i -g @deeeed/metamask-harness@latest)" >&2
63
+ exit 1
64
+ fi
65
+ ;;
66
+ esac
67
+
53
68
  ENSURE_DEPS="$RUNNER_DIR/adapters/shared/ensure-runner-deps.sh"
54
69
  if [ -f "$ENSURE_DEPS" ]; then
55
70
  # shellcheck disable=SC1090
56
71
  bash "$ENSURE_DEPS" "$RUNNER_DIR"
57
72
  fi
58
73
 
59
- ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
60
- ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
61
-
62
74
  find_protocol_root() {
63
75
  local start="$1"
64
76
  local dir
@@ -97,10 +109,38 @@ node_can_run_source_typescript() {
97
109
  rm -rf "$probe_dir"
98
110
  }
99
111
 
112
+ # In a source checkout dist wins when present — make that visible, and warn when
113
+ # the compiled entry is older than the sources it shadows (a stale dist silently
114
+ # runs old code while you edit src/).
115
+ if [ -f "$ENTRY_DIST" ] && [ -f "$ENTRY_TS" ]; then
116
+ export MM_HARNESS_RUN_MODE="dist"
117
+ if find "$RUNNER_DIR/src" -name '*.ts' -newer "$ENTRY_DIST" 2>/dev/null | head -1 | grep -q .; then
118
+ echo "mm-harness: dist/ is OLDER than src/ — this run uses the stale compiled code." >&2
119
+ echo " Next: npm run build (or: trash dist/ to run straight from src via tsx)" >&2
120
+ fi
121
+ elif [ -f "$ENTRY_TS" ]; then
122
+ export MM_HARNESS_RUN_MODE="src"
123
+ fi
124
+
100
125
  if [ -f "$ENTRY_DIST" ]; then
101
126
  exec node "$ENTRY_DIST" "$@"
102
127
  fi
103
128
 
129
+ if [ -f "$ENTRY_TS" ]; then
130
+ case " ${NODE_OPTIONS:-} " in
131
+ *" --conditions=development "*) ;;
132
+ *)
133
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=development"
134
+ ;;
135
+ esac
136
+ fi
137
+
138
+ if [ ! -f "$ENTRY_TS" ]; then
139
+ echo "mm-harness: no CLI entry found (expected dist/mm-harness-cli.js or src/mm-harness-cli.ts)." >&2
140
+ echo " Next: from a source checkout run npm run build, or install dependencies (tsx) for the dev path" >&2
141
+ exit 1
142
+ fi
143
+
104
144
  TSX_BIN="${TSX_BIN:-$RUNNER_DIR/node_modules/.bin/tsx}"
105
145
  if [ ! -x "$TSX_BIN" ] && [ -x "$RUNNER_DIR/../../.bin/tsx" ]; then
106
146
  TSX_BIN="$RUNNER_DIR/../../.bin/tsx"
@@ -126,5 +166,6 @@ if [ -x "$TSX_BIN" ]; then
126
166
  exec "$TSX_BIN" "$ENTRY_TS" "$@"
127
167
  fi
128
168
 
129
- echo "mm-harness source checkout requires dist/mm-harness-cli.js, Node.js TypeScript type stripping, or a local tsx binary. Install dependencies normally, or set TSX_BIN/FARMSLOT_ROOT for local protocol co-development." >&2
169
+ echo "mm-harness: source checkout requires dist/mm-harness-cli.js, Node.js TypeScript type stripping, or a local tsx binary." >&2
170
+ echo " Next: npm run build or yarn install (tsx devDependency) or set TSX_BIN for local protocol co-development" >&2
130
171
  exit 1
@@ -43,6 +43,7 @@ async function assertHealthyExtensionRuntime(options) {
43
43
  }
44
44
  async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
45
45
  const findings = [];
46
+ const warnings = [];
46
47
  let targets = [];
47
48
  try {
48
49
  await jsonGet(`http://127.0.0.1:${cdpPort}/json/version`);
@@ -87,11 +88,12 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort) {
87
88
  findings.push("stateHooks.getPerpsStreamManager is unavailable.");
88
89
  }
89
90
  if (runtime.backgroundProbeOk !== true) {
90
- findings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
91
+ warnings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
91
92
  }
92
93
  const extensionId = safeExtensionId(target);
93
94
  return {
94
95
  status: findings.length === 0 ? "PASS" : "FAIL",
96
+ warnings,
95
97
  cdpPort,
96
98
  targetUrl: target.url,
97
99
  extensionId,
@@ -4,6 +4,7 @@ import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { recipeRuntimeDir } from "../../paths.js";
7
+ import { resolveFarmslotPortsByRepo } from "../../../adapters/shared/resolve-farmslot-ports-core.mjs";
7
8
  const RUNWAY_IOS_METADATA = {
8
9
  artifactName: "ios-app-main-dev-expo",
9
10
  workflow: "expo-dev-build.yml",
@@ -11,6 +12,19 @@ const RUNWAY_IOS_METADATA = {
11
12
  appDirName: "MetaMask.app",
12
13
  fallbackRepo: "MetaMask/metamask-mobile"
13
14
  };
15
+ function resolvePoolIdentity(target) {
16
+ const out = resolveFarmslotPortsByRepo(target);
17
+ const id = {};
18
+ if (!out) return id;
19
+ for (const line of String(out).split("\n")) {
20
+ const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
21
+ if (!m || m[2] === "null" || m[2] === "") continue;
22
+ if (m[1] === "IOS_SIMULATOR") id.simulator = m[2];
23
+ else if (m[1] === "WATCHER_PORT") id.watcherPort = m[2];
24
+ else if (m[1] === "SLOT_ID") id.slotId = m[2];
25
+ }
26
+ return id;
27
+ }
14
28
  async function provisionRunwayMobile(target, options) {
15
29
  const resolvedTarget = path.resolve(target);
16
30
  const platform = options.platform ?? "ios";
@@ -21,7 +35,14 @@ async function provisionRunwayMobile(target, options) {
21
35
  if (platform !== "ios") {
22
36
  return fail(resolvedTarget, platform, "UNSUPPORTED_PLATFORM", "runway provisioning currently installs the iOS .app artifact only.", command, slot);
23
37
  }
24
- const simulator = options.simulator ?? slot.simulator ?? process.env.IOS_SIMULATOR;
38
+ let simulator = options.simulator ?? slot.simulator ?? process.env.IOS_SIMULATOR;
39
+ try {
40
+ const pool = resolvePoolIdentity(resolvedTarget);
41
+ if (!simulator && pool.simulator) simulator = pool.simulator;
42
+ if (!slot.slotId && pool.slotId) slot.slotId = pool.slotId;
43
+ if (!slot.watcherPort && pool.watcherPort) slot.watcherPort = pool.watcherPort;
44
+ } catch {
45
+ }
25
46
  if (!simulator && !options.resolveOnly) {
26
47
  return fail(resolvedTarget, platform, "SIMULATOR_MISSING", "no simulator was resolved from agentic-runtime.json, --simulator, or IOS_SIMULATOR.", command, slot);
27
48
  }
@@ -72,6 +93,7 @@ async function provisionRunwayMobile(target, options) {
72
93
  const cacheRoot = options.cacheRoot ?? defaultRunwayCacheRoot();
73
94
  const cache = ensureCachedArtifact(repo, resolved.branch, resolved.runId, cacheRoot, options);
74
95
  log(options, `runway: installing ${cache.artifact.appPath} on ${sim.name}`);
96
+ bootSimulator(sim.udid ?? sim.name, options);
75
97
  execFileSync("xcrun", ["simctl", "install", sim.udid ?? sim.name, cache.artifact.appPath], { stdio: ["ignore", "ignore", "pipe"] });
76
98
  const baselinePath = writeRunwayBaseline(resolvedTarget, slot, platform, resolved, cache.artifact, sim, false, options.runtimeDir);
77
99
  return {
@@ -379,6 +401,17 @@ function hashPath(root) {
379
401
  visitDirectory(root);
380
402
  return { sizeBytes, sha256: hash.digest("hex") };
381
403
  }
404
+ function bootSimulator(device, options) {
405
+ try {
406
+ execFileSync("xcrun", ["simctl", "boot", device], { stdio: ["ignore", "ignore", "ignore"] });
407
+ } catch {
408
+ }
409
+ try {
410
+ execFileSync("xcrun", ["simctl", "bootstatus", device, "-b"], { stdio: ["ignore", "ignore", "ignore"], timeout: 12e4 });
411
+ } catch {
412
+ log(options, `runway: bootstatus wait for ${device} did not confirm; continuing to install`);
413
+ }
414
+ }
382
415
  function ensureSimulator(name, runtime, deviceType) {
383
416
  const existing = findSimulator(name);
384
417
  if (existing) return { name, udid: existing, created: false, runtime, deviceType };
@@ -14,6 +14,7 @@ function applyKVLines(output, overwrite) {
14
14
  const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
15
15
  if (!m) continue;
16
16
  const [, key, val] = m;
17
+ if (val === "null" || val === "undefined" || val === "") continue;
17
18
  switch (key) {
18
19
  case "WATCHER_PORT":
19
20
  if (overwrite || !process.env["WATCHER_PORT"]) {
@@ -39,15 +40,9 @@ function applyKVLines(output, overwrite) {
39
40
  }
40
41
  function resolveMobileSlotPorts(target) {
41
42
  const ctxOut = resolveMobileRuntimeContext(target);
42
- if (ctxOut?.trim()) {
43
- applyKVLines(ctxOut, true);
44
- return;
45
- }
43
+ if (ctxOut?.trim()) applyKVLines(ctxOut, true);
46
44
  const poolOut = resolveFarmslotPortsByRepo(target);
47
- if (poolOut?.trim()) {
48
- applyKVLines(poolOut, true);
49
- return;
50
- }
45
+ if (poolOut?.trim()) applyKVLines(poolOut, false);
51
46
  const defOut = resolveMobileSlotDefaults(target);
52
47
  if (defOut?.trim()) applyKVLines(defOut, false);
53
48
  }