@deeeed/metamask-harness 0.5.0 → 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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -1,4 +1,5 @@
1
- # resolve-farmslot-ports.sh — resolve CDP + watcher ports for an extension checkout
1
+ #!/usr/bin/env bash
2
+ # resolve-farmslot-ports.sh — bash-compat wrapper over resolve-farmslot-ports.mjs
2
3
  #
3
4
  # Farmslot pool JSON is authoritative when a slot repo matches; otherwise fall back
4
5
  # to the local-extension-N formula (6660+N / 9010+N).
@@ -8,85 +9,15 @@
8
9
  # resolve_extension_runtime_ports /path/to/metamask-extension-N
9
10
  # -> prints CDP_PORT=... WATCHER_PORT=... SLOT_ID=... lines
10
11
 
11
- resolve_farmslot_ports_by_repo() {
12
- local repo="$1"
13
- local farmslot_root="${FARMSLOT_ROOT:-}"
14
- [ -n "$repo" ] || return 1
15
- repo="$(cd "$repo" && pwd -P)"
16
-
17
- local root candidate match
18
- for root in "$farmslot_root" "$HOME/dev/farmslot"; do
19
- [ -n "$root" ] && [ -d "$root/pool" ] || continue
20
- match="$(POOL_ROOT="$root" REPO_TARGET="$repo" python3 - <<'PY'
21
- import glob, json, os, re, sys
22
-
23
- pool_root = os.environ["POOL_ROOT"]
24
- repo = os.path.realpath(os.environ["REPO_TARGET"])
25
- base = os.path.basename(repo)
26
- slot_suffix = None
27
- m = re.search(r"-(\d+)$", base)
28
- if m:
29
- slot_suffix = m.group(1)
30
-
31
- def score_slot(slot):
32
- resources = slot.get("resources") or {}
33
- cdp = (resources.get("browser") or {}).get("cdp_port")
34
- port = (resources.get("dev-server") or {}).get("port")
35
- session = slot.get("session") or ""
36
- slot_id = slot.get("id") or ""
37
- score = 0
38
- if cdp is not None:
39
- score += 100
40
- if port is not None:
41
- score += 10
42
- if slot_suffix:
43
- if session == f"mme-{slot_suffix}":
44
- score += 50
45
- if slot_id.endswith(f"mme-{slot_suffix}") or f"-mme-{slot_suffix}" in slot_id:
46
- score += 40
47
- if "demo" not in slot_id.lower():
48
- score += 5
49
- return score, cdp, port, slot_id
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ _RESOLVE_PORTS_MJS="$SCRIPT_DIR/resolve-farmslot-ports.mjs"
50
14
 
51
- best = None
52
- for path in sorted(glob.glob(os.path.join(pool_root, "pool", "*.json"))):
53
- if ".bak." in os.path.basename(path):
54
- continue
55
- try:
56
- data = json.load(open(path, encoding="utf-8"))
57
- except Exception:
58
- continue
59
- for slot in data.get("slots") or []:
60
- slot_repo = slot.get("repo") or ""
61
- if not slot_repo:
62
- continue
63
- try:
64
- if os.path.realpath(slot_repo) != repo:
65
- continue
66
- except Exception:
67
- continue
68
- scored = score_slot(slot)
69
- if best is None or scored[0] > best[0]:
70
- best = scored
71
-
72
- if best is None:
73
- sys.exit(1)
15
+ _resolve_ports_cli() {
16
+ node "$_RESOLVE_PORTS_MJS" "$1" "${2:-.}"
17
+ }
74
18
 
75
- _, cdp, port, slot_id = best
76
- if cdp is not None:
77
- print(f"CDP_PORT={cdp}")
78
- if port is not None:
79
- print(f"WATCHER_PORT={port}")
80
- if slot_id:
81
- print(f"SLOT_ID={slot_id}")
82
- sys.exit(0)
83
- PY
84
- )" || continue
85
- [ -n "$match" ] || continue
86
- printf '%s\n' "$match"
87
- return 0
88
- done
89
- return 1
19
+ resolve_farmslot_ports_by_repo() {
20
+ _resolve_ports_cli resolve_farmslot_ports_by_repo "$1"
90
21
  }
91
22
 
92
23
  infer_extension_slot_suffix() {
@@ -100,27 +31,18 @@ infer_extension_slot_suffix() {
100
31
  }
101
32
 
102
33
  resolve_default_extension_ports() {
103
- local repo="$1" n
104
- n="$(infer_extension_slot_suffix "$repo" || true)"
105
- [ -n "$n" ] || return 1
106
- printf 'CDP_PORT=%s\n' "$((6660 + n))"
107
- printf 'WATCHER_PORT=%s\n' "$((9010 + n))"
108
- printf 'SLOT_ID=local-extension-%s\n' "$n"
34
+ _resolve_ports_cli resolve_default_extension_ports "$1"
109
35
  }
110
36
 
111
37
  resolve_extension_runtime_ports() {
112
- local repo="${1:-.}"
113
- if resolve_farmslot_ports_by_repo "$repo"; then
114
- return 0
115
- fi
116
- resolve_default_extension_ports "$repo"
38
+ _resolve_ports_cli resolve_extension_runtime_ports "${1:-.}"
117
39
  }
118
40
 
119
41
  apply_resolved_extension_ports() {
120
42
  local repo="${1:-.}" line key val from_pool=false resolved=""
121
- if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
43
+ if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
122
44
  from_pool=true
123
- elif resolved="$(resolve_default_extension_ports "$repo" 2>/dev/null)"; then
45
+ elif resolved="$(_resolve_ports_cli resolve_default_extension_ports "$repo" 2>/dev/null)"; then
124
46
  from_pool=false
125
47
  else
126
48
  return 0
@@ -143,52 +65,23 @@ apply_resolved_extension_ports() {
143
65
  done <<< "$resolved"
144
66
  }
145
67
 
146
- # Mobile slot defaults: derive Metro port + iOS simulator name from directory suffix.
147
- # Port formula: 8060 + N (slot 1 → 8061). iOS simulator: mm-N.
148
- # Mirrors the extension convention (6660+N / 9010+N) for multi-slot mobile farms.
149
68
  resolve_mobile_slot_defaults() {
150
- local repo="$1" n
151
- n="$(infer_extension_slot_suffix "$repo" || true)"
152
- [ -n "$n" ] || return 1
153
- printf 'WATCHER_PORT=%s\n' "$((8060 + n))"
154
- printf 'IOS_SIMULATOR=mm-%s\n' "$n"
155
- printf 'SLOT_ID=local-mobile-%s\n' "$n"
69
+ _resolve_ports_cli resolve_mobile_slot_defaults "$1"
156
70
  }
157
71
 
158
- # Slot context the orchestrator's prepare wrote INTO the checkout — the
159
- # authoritative device/port mapping for this repo (survives pool renames that
160
- # break any naming formula). Highest-priority detection source.
161
72
  resolve_mobile_runtime_context() {
162
- local repo="$1" ctx
163
- ctx="$repo/${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/agentic-runtime.json"
164
- [ -f "$ctx" ] || return 1
165
- node -e '
166
- try {
167
- const c = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
168
- if (!c.simulator && !c.metroPort) process.exit(1);
169
- if (c.metroPort) console.log(`WATCHER_PORT=${c.metroPort}`);
170
- if (c.simulator) console.log(`IOS_SIMULATOR=${c.simulator}`);
171
- if (c.slotId) console.log(`SLOT_ID=${c.slotId}`);
172
- } catch { process.exit(1); }
173
- ' "$ctx" 2>/dev/null
73
+ _resolve_ports_cli resolve_mobile_runtime_context "$1"
174
74
  }
175
75
 
176
76
  resolve_mobile_runtime_ports() {
177
- local repo="${1:-.}"
178
- if resolve_mobile_runtime_context "$repo"; then
179
- return 0
180
- fi
181
- if resolve_farmslot_ports_by_repo "$repo"; then
182
- return 0
183
- fi
184
- resolve_mobile_slot_defaults "$repo"
77
+ _resolve_ports_cli resolve_mobile_runtime_ports "${1:-.}"
185
78
  }
186
79
 
187
80
  apply_resolved_mobile_ports() {
188
81
  local repo="${1:-.}" line key val from_pool=false resolved=""
189
- if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
82
+ if resolved="$(_resolve_ports_cli resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
190
83
  from_pool=true
191
- elif resolved="$(resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
84
+ elif resolved="$(_resolve_ports_cli resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
192
85
  from_pool=false
193
86
  else
194
87
  return 0
@@ -209,4 +102,4 @@ apply_resolved_mobile_ports() {
209
102
  ;;
210
103
  esac
211
104
  done <<< "$resolved"
212
- }
105
+ }
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 };
@@ -103,6 +103,8 @@ async function decideMobileReadiness(target, options = {}) {
103
103
  const fast = options.preflightMode === "fast";
104
104
  const report = await computeMobileReadiness(resolved, options, fast);
105
105
  if (fast && report.decision === "install") {
106
+ const hasNodeModules = fs.existsSync(path.join(resolved, "node_modules"));
107
+ const standalone = hasNodeModules ? "`yarn setup:expo --no-build-ios --no-build-android`" : "`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`";
106
108
  return {
107
109
  ...report,
108
110
  decision: "blocked",
@@ -111,7 +113,7 @@ async function decideMobileReadiness(target, options = {}) {
111
113
  "Fast preflight found dependencies not ready and does not install them (the orchestrator deps phase owns installation).",
112
114
  ...report.reasons
113
115
  ],
114
- userAction: "run the slot deps/prepare phase; standalone: `yarn setup:expo --no-build-ios --no-build-android` in the checkout, or `mm-harness launch <platform> --build` to install and build",
116
+ userAction: `run the slot deps/prepare phase; standalone: ${standalone} in the checkout, then re-run \`mm-harness launch <platform>\``,
115
117
  actions: []
116
118
  };
117
119
  }
@@ -0,0 +1,22 @@
1
+ import {
2
+ formatKvLines,
3
+ inferSlotSuffix,
4
+ realRepoPath,
5
+ resolveDefaultExtensionPorts,
6
+ resolveExtensionRuntimePorts,
7
+ resolveFarmslotPortsByRepo,
8
+ resolveMobileRuntimeContext,
9
+ resolveMobileRuntimePorts,
10
+ resolveMobileSlotDefaults
11
+ } from "../../adapters/shared/resolve-farmslot-ports-core.mjs";
12
+ export {
13
+ formatKvLines,
14
+ inferSlotSuffix,
15
+ realRepoPath,
16
+ resolveDefaultExtensionPorts,
17
+ resolveExtensionRuntimePorts,
18
+ resolveFarmslotPortsByRepo,
19
+ resolveMobileRuntimeContext,
20
+ resolveMobileRuntimePorts,
21
+ resolveMobileSlotDefaults
22
+ };
@@ -2,12 +2,19 @@ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { readRuntimeContextField, resolveRuntimeContextPath } from "../harness.js";
5
- import { recipeRuntimeDir, runnerDir } from "../paths.js";
5
+ import { recipeRuntimeDir } from "../paths.js";
6
+ import {
7
+ resolveDefaultExtensionPorts,
8
+ resolveFarmslotPortsByRepo,
9
+ resolveMobileRuntimeContext,
10
+ resolveMobileSlotDefaults
11
+ } from "./resolve-farmslot-ports.js";
6
12
  function applyKVLines(output, overwrite) {
7
13
  for (const line of output.split("\n")) {
8
14
  const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
9
15
  if (!m) continue;
10
16
  const [, key, val] = m;
17
+ if (val === "null" || val === "undefined" || val === "") continue;
11
18
  switch (key) {
12
19
  case "WATCHER_PORT":
13
20
  if (overwrite || !process.env["WATCHER_PORT"]) {
@@ -31,31 +38,13 @@ function applyKVLines(output, overwrite) {
31
38
  }
32
39
  }
33
40
  }
34
- function sourceResolve(fn, target) {
35
- const resolveScript = path.join(runnerDir, "adapters/shared/resolve-farmslot-ports.sh");
36
- try {
37
- return execFileSync("bash", ["-c", `source "${resolveScript}" && ${fn} "${target}"`], {
38
- encoding: "utf8",
39
- timeout: 5e3,
40
- stdio: ["ignore", "pipe", "ignore"]
41
- });
42
- } catch {
43
- return "";
44
- }
45
- }
46
41
  function resolveMobileSlotPorts(target) {
47
- const ctxOut = sourceResolve("resolve_mobile_runtime_context", target);
48
- if (ctxOut.trim()) {
49
- applyKVLines(ctxOut, true);
50
- return;
51
- }
52
- const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
53
- if (poolOut.trim()) {
54
- applyKVLines(poolOut, true);
55
- return;
56
- }
57
- const defOut = sourceResolve("resolve_mobile_slot_defaults", target);
58
- if (defOut.trim()) applyKVLines(defOut, false);
42
+ const ctxOut = resolveMobileRuntimeContext(target);
43
+ if (ctxOut?.trim()) applyKVLines(ctxOut, true);
44
+ const poolOut = resolveFarmslotPortsByRepo(target);
45
+ if (poolOut?.trim()) applyKVLines(poolOut, false);
46
+ const defOut = resolveMobileSlotDefaults(target);
47
+ if (defOut?.trim()) applyKVLines(defOut, false);
59
48
  }
60
49
  function resolveExtensionSlotPorts(target) {
61
50
  const contextPath = resolveRuntimeContextPath(target);
@@ -70,13 +59,13 @@ function resolveExtensionSlotPorts(target) {
70
59
  process.env["RECIPE_WATCHER_PORT"] = dev;
71
60
  }
72
61
  if (process.env["CDP_PORT"]) return;
73
- const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
74
- if (poolOut.trim()) {
62
+ const poolOut = resolveFarmslotPortsByRepo(target);
63
+ if (poolOut?.trim()) {
75
64
  applyKVLines(poolOut, true);
76
65
  return;
77
66
  }
78
- const defOut = sourceResolve("resolve_default_extension_ports", target);
79
- if (defOut.trim()) applyKVLines(defOut, false);
67
+ const defOut = resolveDefaultExtensionPorts(target);
68
+ if (defOut?.trim()) applyKVLines(defOut, false);
80
69
  }
81
70
  function stopExtensionWatcher(target) {
82
71
  const runtimeAbs = path.join(target, recipeRuntimeDir());