@deeeed/metamask-harness 0.5.1 → 0.6.1

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 (90) hide show
  1. package/CHANGELOG.md +47 -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/start-metro.sh +6 -0
  12. package/adapters/mobile/stop-metro.sh +20 -0
  13. package/adapters/shared/cli-ux.sh +20 -2
  14. package/adapters/shared/ensure-runner-deps.sh +30 -0
  15. package/adapters/shared/reap-checkout-metros.sh +58 -0
  16. package/adapters/shared/recipe-harness-root.mjs +23 -0
  17. package/adapters/shared/resolve-farmslot-ports-core.mjs +15 -6
  18. package/bin/mm-harness +45 -4
  19. package/dist/adapters/extension/runtime.js +3 -1
  20. package/dist/adapters/mobile/provision.js +34 -1
  21. package/dist/adapters/slot-ports.js +3 -8
  22. package/dist/cli.js +32 -1463
  23. package/dist/commands/call.js +183 -0
  24. package/dist/commands/completion-candidates.js +58 -0
  25. package/dist/commands/doctor.js +101 -0
  26. package/dist/commands/ensure-ready.js +24 -0
  27. package/dist/commands/flows.js +62 -0
  28. package/dist/commands/launch/extension.js +40 -0
  29. package/dist/commands/{launch.js → launch/index.js} +15 -47
  30. package/dist/commands/launch/mobile.js +10 -0
  31. package/dist/commands/manifest.js +72 -0
  32. package/dist/commands/parse-args.js +189 -0
  33. package/dist/commands/provision.js +136 -0
  34. package/dist/commands/resolve-extension.js +23 -0
  35. package/dist/commands/run-engine.js +341 -0
  36. package/dist/commands/run.js +217 -0
  37. package/dist/commands/runtime-decision.js +58 -0
  38. package/dist/commands/runtime-health.js +25 -0
  39. package/dist/commands/runtime-launch.js +139 -0
  40. package/dist/commands/self-test.js +52 -0
  41. package/dist/commands/stop.js +52 -0
  42. package/dist/harness.js +8 -48
  43. package/dist/mm-harness-cli.js +13 -8
  44. package/docs/CLI-SPEC.md +1 -1
  45. package/docs/CODE-MAP.md +62 -0
  46. package/library/README.md +14 -0
  47. package/library/actions/extension/platform/cdp.mjs +1 -1
  48. package/library/actions/extension/wallet/ensure_unlocked.mjs +6 -0
  49. package/library/actions/mobile/wallet/ensure_unlocked.mjs +13 -1
  50. package/package.json +6 -7
  51. package/src/adapters/core/surface.ts +0 -71
  52. package/src/adapters/extension/ensure-ready.ts +0 -185
  53. package/src/adapters/extension/extension-id.ts +0 -107
  54. package/src/adapters/extension/runtime-decision.ts +0 -445
  55. package/src/adapters/extension/runtime.ts +0 -407
  56. package/src/adapters/extension/surface.ts +0 -88
  57. package/src/adapters/mobile/deps-markers.ts +0 -21
  58. package/src/adapters/mobile/prepare.ts +0 -246
  59. package/src/adapters/mobile/provision.ts +0 -594
  60. package/src/adapters/mobile/runtime-decision.ts +0 -466
  61. package/src/adapters/mobile/surface.ts +0 -71
  62. package/src/adapters/resolve-farmslot-ports.ts +0 -13
  63. package/src/adapters/slot-ports.ts +0 -158
  64. package/src/adapters/surface.ts +0 -117
  65. package/src/adapters.ts +0 -601
  66. package/src/cli-color.ts +0 -92
  67. package/src/cli-commands.ts +0 -250
  68. package/src/cli-version.ts +0 -141
  69. package/src/cli.ts +0 -2091
  70. package/src/commands/debug.ts +0 -65
  71. package/src/commands/fixtures.ts +0 -198
  72. package/src/commands/launch.ts +0 -470
  73. package/src/commands/logs.ts +0 -99
  74. package/src/commands/shared.ts +0 -235
  75. package/src/commands/update.ts +0 -316
  76. package/src/completions-cache.ts +0 -86
  77. package/src/doctor.ts +0 -215
  78. package/src/harness.ts +0 -797
  79. package/src/heal-bounds.ts +0 -198
  80. package/src/index.ts +0 -15
  81. package/src/leaf-invoke.ts +0 -28
  82. package/src/live-adapter-contract.ts +0 -274
  83. package/src/manifest.ts +0 -47
  84. package/src/mm-harness-cli.ts +0 -655
  85. package/src/paths.ts +0 -198
  86. package/src/progress.ts +0 -117
  87. package/src/recording-target.ts +0 -147
  88. package/src/run-recording.ts +0 -329
  89. package/src/runner.ts +0 -108
  90. 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
  }