@deeeed/metamask-harness 0.5.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,13 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## 0.5.0 - 2026-07-05
3
+ ## 0.5.1 - 2026-07-05
4
4
 
5
- Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
5
+ Fresh-install hotfixes found in live validation of 0.5.0.
6
+
7
+ ### Fixed
8
+ - **First launch on a provisioned slot targets the slot's own simulator** — the mobile resolution ladder now reads the `runway-provision.json` baseline (simulator name/udid, watcher port, slot id) when `agentic-runtime.json` doesn't exist yet, and boots that simulator. Previously a provisioned-but-unprepared slot degraded to the simctl `booted` alias, missed the installed dev client, and guessed the wrong Metro port.
9
+ - **`call` works from published installs** — library actions load harness helpers through a dist-preferring bridge instead of importing `src/index.ts` (node refuses to type-strip `.ts` under `node_modules`). The bridge is deliberately narrow (paths + run-recording) to avoid an import cycle through `adapters.ts`.
10
+ - **deps-not-ready guidance matches the caller's state** — on a checkout without node_modules the taught command now leads with `yarn install --immutable` (yarn cannot run scripts at all on a bare checkout), and always ends with the launch re-run.
6
11
 
12
+ ## Unreleased
13
+
14
+ ### Changed
15
+ - **resolve-farmslot-ports is TypeScript-first** — pool/context/formula port resolution lives in self-contained `adapters/shared/resolve-farmslot-ports-core.mjs` (works from runner and injected overlay copies); `src/adapters/resolve-farmslot-ports.ts` re-exports it for `slot-ports.ts`. `resolve-farmslot-ports.mjs` is the node leaf; `resolve-farmslot-ports.sh` remains a thin bash-compat wrapper for sourced callers.
16
+
17
+ ## 0.5.0 - 2026-07-05
18
+ Minor release: thin Runway provisioning (harness-owned slot provisioning with a shared artifact cache) plus compiled-dist packaging.
7
19
  ### Added
8
20
  - **`mm-harness provision runway`** — provisions a mobile slot with exactly: resolve the latest Runway (expo-dev-build) artifact, download it ONCE into a shared machine cache (`~/.cache/metamask-harness/runway/<runId>/`, sha256+size integrity, corrupt entries re-downloaded, never a slot failure), create the simulator if missing, install the dev client on it. Zero yarn/deps/Metro — those stay dispatch-time (lazy, presence-authoritative readiness unchanged). `install --runway` is the overlay-path equivalent (covered by its own contract test). Baseline recorded so doctor reports provisioned-but-deps-pending as a normal state.
9
21
  - **Compiled `dist/` for published installs** — the npm tarball ships esbuild-compiled JS; `npm i -g` installs run the dist entry (~0.21s cold vs ~0.37s tsx). Source checkouts without `dist/` keep the tsx dev path; `bin/mm-harness` prefers dist when present. Packaging contract test (pack → global install → dist entry runs).
10
-
11
22
  ### Changed
12
23
  - Runway provisioning callers (farm installer, mobile-farm `runway` prepare profile) delegate to the harness; the profile no longer runs a deps phase and a contract test enforces that.
13
24
 
@@ -50,6 +61,9 @@ Minor release: the adapter-surface architecture plus the day's feature wave (0.3
50
61
  - Add `mm-harness provision runway ios --resolve-only` to return Runway run/revision/artifact metadata without touching simulator, cache, download, install, or baseline state.
51
62
  - **Dist publish path** — `npm run build` compiles `src/` to `dist/`; `npm pack` / global install runs the compiled CLI entry (`dist/mm-harness-cli.js`) while a source checkout without `dist/` keeps the tsx+src dev path. Contracts: `packaging-dist-install`, `packaging-dev-checkout`.
52
63
 
64
+ ### Changed
65
+ - **Leaf stderr hints use mm-harness verbs** — Metro/webpack compact-log prefixes, log-tail hints, and speed-ladder copy now point at `mm-harness logs`, `mm-harness doctor`, `mm-harness launch`, and `mm-harness install` instead of retired `recipe *` commands. Contract: `leaf-branding-retired`.
66
+
53
67
  ### Fixed
54
68
  - `mm-harness provision runway ios` now routes through the same Runway provisioning surface as `install --runway`, so overlay dispatch accepts the compatibility command instead of rejecting it as an unsupported harness action.
55
69
  - Runway provisioning now accepts farm context flags (`--slot`, `--watcher-port`, `--runtime-dir`), records slot/watcher/runtime-dir baselines correctly, ignores stale provision baselines unless the app is still installed on the simulator, and emits rerunnable `Next:` commands from the actual invocation.
@@ -108,6 +108,8 @@ copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join
108
108
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
109
109
  copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
110
110
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
111
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
112
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
111
113
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
112
114
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
113
115
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
@@ -97,7 +97,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
97
97
  exit 1
98
98
  }
99
99
  else
100
- echo "[refresh] activate-repo-node.sh missing from harness; run: recipe sync" >&2
100
+ echo "[refresh] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
101
101
  exit 1
102
102
  fi
103
103
 
@@ -186,5 +186,5 @@ kill "$BUILD_PID" 2>/dev/null || true
186
186
  wait "$BUILD_PID" 2>/dev/null || true
187
187
  trap - EXIT
188
188
 
189
- echo "[refresh] Build refreshed and frozen — run: recipe reopen"
189
+ echo "[refresh] Build refreshed and frozen — run: mm-harness launch"
190
190
  echo "[refresh] Log kept at: $LOG"
@@ -82,7 +82,7 @@ elif command -v activate_repo_node >/dev/null 2>&1; then
82
82
  exit 1
83
83
  }
84
84
  else
85
- echo "[recipe-harness] activate-repo-node.sh missing from harness; run: recipe sync" >&2
85
+ echo "[recipe-harness] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
86
86
  exit 1
87
87
  fi
88
88
  if [ -z "$WATCHER_PORT" ]; then
@@ -233,7 +233,7 @@ for _lib in "$SCRIPT_DIR/lib" "$SCRIPT_DIR/../shared"; do
233
233
  done
234
234
  unset _lib
235
235
  echo "[recipe-harness] Starting yarn start (watcher-port=${WATCHER_PORT:-default})"
236
- echo "[recipe-harness] compact build view - full log: $watch_log (recipe logs --full)"
236
+ echo "[recipe-harness] compact build view - full log: $watch_log (mm-harness logs --full)"
237
237
  started_pid=""
238
238
  # Launch the watcher detached writing to the log; the tmux window is a read-only tail viewer.
239
239
  path_prefix="${REPO_NODE_PATH_PREFIX:-}"
@@ -308,12 +308,28 @@
308
308
  },
309
309
  {
310
310
  "id": "lib/resolve-farmslot-ports",
311
- "entry": "adapters/shared/resolve-farmslot-ports.sh",
312
- "kind": "lib",
311
+ "entry": "adapters/shared/resolve-farmslot-ports.mjs",
312
+ "kind": "node",
313
313
  "purpose": "Resolve CDP + watcher ports from farmslot pool JSON by repo path, else 6660+N / 9010+N.",
314
- "inputs": "resolve_extension_runtime_ports <repo>",
314
+ "inputs": "resolve-farmslot-ports.mjs resolve_extension_runtime_ports <repo>",
315
315
  "outputs": "CDP_PORT / WATCHER_PORT / SLOT_ID lines on stdout"
316
316
  },
317
+ {
318
+ "id": "lib/resolve-farmslot-ports-core",
319
+ "entry": "adapters/shared/resolve-farmslot-ports-core.mjs",
320
+ "kind": "lib",
321
+ "purpose": "Self-contained pool/context/formula port resolution imported by the node leaf and TS surface.",
322
+ "inputs": "ESM import from resolve-farmslot-ports.mjs or resolve-farmslot-ports.ts",
323
+ "outputs": "resolveFarmslotPortsByRepo and related functions"
324
+ },
325
+ {
326
+ "id": "lib/resolve-farmslot-ports-sh",
327
+ "entry": "adapters/shared/resolve-farmslot-ports.sh",
328
+ "kind": "lib",
329
+ "purpose": "Bash-compat sourced wrapper over resolve-farmslot-ports.mjs for stop-metro and legacy callers.",
330
+ "inputs": "source + resolve_* / apply_resolved_* function calls",
331
+ "outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
332
+ },
317
333
  {
318
334
  "id": "lib/sync-wallet-fixture",
319
335
  "entry": "adapters/shared/sync-wallet-fixture.sh",
@@ -102,7 +102,7 @@ CLEAR_SUFFIX=""
102
102
 
103
103
  printf 'Starting Metro on port %s (workers=%s%s)\n' \
104
104
  "$PORT" "$METRO_WORKERS" "${CLEAR:+, clear}" >&2
105
- printf '(Full log: %s — recipe logs | recipe logs --full)\n' "$LOG_FILE" >&2
105
+ printf '(Full log: %s — mm-harness logs | mm-harness logs --full)\n' "$LOG_FILE" >&2
106
106
 
107
107
  # Metro runs detached, writing to the log. The tmux window is a read-only tail.
108
108
  (
@@ -296,7 +296,7 @@ if (checked === 0) {
296
296
  const parts = [];
297
297
  if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
298
298
  if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
299
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or recipe sync to refresh the in-repo HUD/AgenticService from the runner' }));
299
+ process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
300
300
  } else {
301
301
  process.stdout.write(JSON.stringify({ name, status: 'pass' }));
302
302
  }
@@ -134,7 +134,7 @@ activate_repo_node() {
134
134
  require_repo_node() {
135
135
  local target="${1:-.}"
136
136
  if ! command -v activate_repo_node >/dev/null 2>&1; then
137
- echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (recipe sync)" >&2
137
+ echo "[env] activate-repo-node.sh is not sourced; reinstall the harness (mm-harness install)" >&2
138
138
  return 1
139
139
  fi
140
140
  activate_repo_node "$target" || {
@@ -67,7 +67,7 @@ recipe_instant_hint() {
67
67
  local message="$1"
68
68
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && return 0
69
69
  [ -t 2 ] || return 0
70
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
70
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
71
71
  printf '%s: %s\n' \
72
72
  "$(recipe_color label "$prefix")" \
73
73
  "$(recipe_color dim "$message")" >&2
@@ -92,7 +92,7 @@ recipe_done() {
92
92
  recipe_failure_playbook() {
93
93
  local verb="$1" reason="$2"
94
94
  shift 2
95
- local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-recipe}"
95
+ local log_path="" artifact="" prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
96
96
  while [ "$#" -gt 0 ]; do
97
97
  case "$1" in
98
98
  --log) log_path="$2"; shift 2 ;;
@@ -106,31 +106,32 @@ recipe_failure_playbook() {
106
106
  "$(recipe_color cmd "$verb")" \
107
107
  "$(recipe_color err failed)" \
108
108
  "$(recipe_color warn "$reason")" >&2
109
- printf '%s\n' "$(recipe_color label next:)" >&2
109
+ printf '%s\n' "$(recipe_color label "next:")" >&2
110
110
  case "$verb" in
111
111
  rebuild|up|watch)
112
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
113
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
114
- printf ' %s\n' "$(recipe_color cmd "$prefix doctor")" >&2
115
- printf ' %s\n' "$(recipe_color cmd "$prefix sync")" >&2
112
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
113
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
114
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
115
+ printf ' %s\n' "$(recipe_color cmd "mm-harness install")" >&2
116
116
  ;;
117
117
  refresh)
118
- printf ' %s\n' "$(recipe_color cmd "$prefix watch")" >&2
119
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
120
- printf ' %s\n' "$(recipe_color cmd "$prefix rebuild")" >&2
118
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
119
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
120
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch --build")" >&2
121
121
  ;;
122
122
  metro|ios|android|setup-wallet)
123
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
124
- printf ' %s\n' "$(recipe_color cmd "$prefix logs --full")" >&2
125
- printf ' %s\n' "$(recipe_color cmd "$prefix up")" >&2
123
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
124
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs --full")" >&2
125
+ printf ' %s\n' "$(recipe_color cmd "mm-harness launch")" >&2
126
126
  ;;
127
127
  *)
128
- printf ' %s\n' "$(recipe_color cmd "$prefix status")" >&2
129
- printf ' %s\n' "$(recipe_color cmd "$prefix logs")" >&2
128
+ printf ' %s\n' "$(recipe_color cmd "mm-harness doctor")" >&2
129
+ printf ' %s\n' "$(recipe_color cmd "mm-harness logs")" >&2
130
130
  ;;
131
131
  esac
132
132
  [ -n "$log_path" ] && printf '%s\n' "$(recipe_color path "log: $log_path")" >&2
133
133
  [ -n "$artifact" ] && printf '%s\n' "$(recipe_color path "artifact: $artifact")" >&2
134
+ return 0
134
135
  }
135
136
 
136
137
  recipe_check_harness_stale() {
@@ -152,12 +153,12 @@ recipe_check_harness_stale() {
152
153
 
153
154
  recipe_warn_harness_stale() {
154
155
  local adapter="$1" root="$2" harness_root="$3" runner_root="$4"
155
- local prefix="${RECIPE_CLI_PREFIX:-recipe}"
156
+ local prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
156
157
  if recipe_check_harness_stale "$adapter" "$root" "$harness_root" "$runner_root"; then
157
158
  printf '%s: %s - run: %s\n' \
158
159
  "$(recipe_color label "$prefix")" \
159
160
  "$(recipe_color warn 'harness scripts are stale')" \
160
- "$(recipe_color cmd "$prefix sync")" >&2
161
+ "$(recipe_color cmd "mm-harness install")" >&2
161
162
  return 1
162
163
  fi
163
164
  return 0
@@ -190,7 +191,7 @@ recipe_try_farmslot_fixture_sync() {
190
191
  recipe_sync_wallet_fixture() {
191
192
  local root="$1" runtime_dir="$2" cdp_port="${3:-}" seed_script="${4:-}" farmslot_slot="${5:-}"
192
193
  local canonical="$root/$runtime_dir/wallet-fixture.json"
193
- local candidate resolved prefix="${RECIPE_CLI_PREFIX:-recipe}"
194
+ local candidate resolved prefix="${RECIPE_CLI_PREFIX:-mm-harness}"
194
195
 
195
196
  mkdir -p "$root/$runtime_dir"
196
197
  if [ -f "$canonical" ]; then
@@ -239,10 +240,10 @@ recipe_sync_wallet_fixture() {
239
240
  recipe_speed_ladder() {
240
241
  cat >&2 <<'EOF'
241
242
  Speed ladder:
242
- FAST (seconds): recipe watch -> recipe refresh
243
- SLOW (minutes): recipe rebuild
244
- INSPECT: recipe status | recipe ports | recipe logs | recipe logs --full | recipe debug
245
- PROOF: recipe run <recipe.json>
246
- SYNC CONTEXT: recipe sync
243
+ FAST (seconds): mm-harness launch (extension quick relaunch)
244
+ SLOW (minutes): mm-harness launch --build
245
+ INSPECT: mm-harness doctor | mm-harness logs | mm-harness logs --full | mm-harness debug
246
+ PROOF: mm-harness run <recipe.json>
247
+ SYNC CONTEXT: mm-harness fixtures sync
247
248
  EOF
248
249
  }
@@ -153,9 +153,9 @@ function readNewContent(file, offset) {
153
153
 
154
154
  function renderHeader(label, logPath, title) {
155
155
  return [
156
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
- `${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: recipe logs --full', { stream: process.stderr })}`,
156
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('bold', title || label, { stream: process.stderr })}`,
157
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('path', `full log: ${logPath}`, { stream: process.stderr })}`,
158
+ `${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('comment', 'full stream: mm-harness logs --full', { stream: process.stderr })}`,
159
159
  '',
160
160
  ];
161
161
  }
@@ -232,7 +232,7 @@ async function watchLog(options) {
232
232
  renderCompact({ label, logPath, events, quiet: mode === 'quiet' });
233
233
  if (mode !== 'quiet') {
234
234
  const elapsed = Math.round((Date.now() - started) / 1000);
235
- process.stderr.write(`${color('label', 'recipe', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
235
+ process.stderr.write(`${color('label', 'mm-harness', { stream: process.stderr })} ${color('dim', '|', { stream: process.stderr })} ${color('ok', `build complete (${elapsed}s)`, { stream: process.stderr })}\n`);
236
236
  }
237
237
  return 0;
238
238
  }
@@ -221,7 +221,7 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
221
221
  error: 'cdp-unreachable',
222
222
  inspectUrl,
223
223
  configureHint,
224
- hint: `Start the extension runtime first (recipe reopen). Then open chrome://inspect and add ${configureHint}.`,
224
+ hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
225
225
  detail: error instanceof Error ? error.message : String(error),
226
226
  };
227
227
  }
@@ -0,0 +1,196 @@
1
+ // Farmslot pool + slot-suffix port resolution — pure JS for runner and overlay leaves.
2
+
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const coreDir = path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ function pathDefault(key) {
11
+ for (const candidate of [
12
+ path.join(coreDir, 'path-defaults.json'),
13
+ path.join(coreDir, '../../adapters/shared/path-defaults.json'),
14
+ ]) {
15
+ if (!fs.existsSync(candidate)) continue;
16
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
17
+ if (value) return value;
18
+ }
19
+ throw new Error(`Missing path default: ${key}`);
20
+ }
21
+
22
+ function recipeRuntimeDir() {
23
+ return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
24
+ }
25
+
26
+ export function formatKvLines(kv) {
27
+ const lines = [];
28
+ if (kv.CDP_PORT !== undefined) lines.push(`CDP_PORT=${kv.CDP_PORT}`);
29
+ if (kv.WATCHER_PORT !== undefined) lines.push(`WATCHER_PORT=${kv.WATCHER_PORT}`);
30
+ if (kv.SLOT_ID) lines.push(`SLOT_ID=${kv.SLOT_ID}`);
31
+ if (kv.IOS_SIMULATOR) lines.push(`IOS_SIMULATOR=${kv.IOS_SIMULATOR}`);
32
+ return lines.length ? `${lines.join('\n')}\n` : '';
33
+ }
34
+
35
+ export function realRepoPath(repo) {
36
+ if (!repo) return null;
37
+ try {
38
+ return fs.realpathSync(repo);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function inferSlotSuffix(repo) {
45
+ const base = path.basename(repo);
46
+ const m = /-(\d+)$/u.exec(base);
47
+ return m ? m[1] : null;
48
+ }
49
+
50
+ function scoreSlot(slot, slotSuffix) {
51
+ const resources = slot.resources ?? {};
52
+ const cdp = resources.browser?.cdp_port;
53
+ const port = resources['dev-server']?.port;
54
+ const session = slot.session ?? '';
55
+ const slotId = slot.id ?? '';
56
+ let score = 0;
57
+ if (cdp !== undefined) score += 100;
58
+ if (port !== undefined) score += 10;
59
+ if (slotSuffix) {
60
+ if (session === `mme-${slotSuffix}`) score += 50;
61
+ if (slotId.endsWith(`mme-${slotSuffix}`) || slotId.includes(`-mme-${slotSuffix}`)) score += 40;
62
+ }
63
+ if (!slotId.toLowerCase().includes('demo')) score += 5;
64
+ return [score, cdp, port, slotId];
65
+ }
66
+
67
+ export function resolveFarmslotPortsByRepo(repo) {
68
+ const realRepo = realRepoPath(repo);
69
+ if (!realRepo) return null;
70
+
71
+ const slotSuffix = inferSlotSuffix(realRepo);
72
+ const roots = [];
73
+ if (process.env.FARMSLOT_ROOT) roots.push(process.env.FARMSLOT_ROOT);
74
+ roots.push(path.join(os.homedir(), 'dev', 'farmslot'));
75
+
76
+ for (const root of roots) {
77
+ if (!root || !fs.existsSync(path.join(root, 'pool'))) continue;
78
+ const poolDir = path.join(root, 'pool');
79
+ let best = null;
80
+
81
+ for (const name of fs.readdirSync(poolDir).sort()) {
82
+ if (!name.endsWith('.json') || name.includes('.bak.')) continue;
83
+ let data;
84
+ try {
85
+ data = JSON.parse(fs.readFileSync(path.join(poolDir, name), 'utf8'));
86
+ } catch {
87
+ continue;
88
+ }
89
+ for (const slot of data.slots ?? []) {
90
+ const slotRepo = slot.repo ?? '';
91
+ if (!slotRepo) continue;
92
+ let slotReal;
93
+ try {
94
+ slotReal = fs.realpathSync(slotRepo);
95
+ } catch {
96
+ continue;
97
+ }
98
+ if (slotReal !== realRepo) continue;
99
+ const scored = scoreSlot(slot, slotSuffix);
100
+ if (!best || scored[0] > best[0]) best = scored;
101
+ }
102
+ }
103
+
104
+ if (best) {
105
+ const [, cdp, port, slotId] = best;
106
+ return formatKvLines({
107
+ ...(cdp !== undefined ? { CDP_PORT: cdp } : {}),
108
+ ...(port !== undefined ? { WATCHER_PORT: port } : {}),
109
+ ...(slotId ? { SLOT_ID: slotId } : {}),
110
+ });
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ export function resolveDefaultExtensionPorts(repo) {
117
+ const n = inferSlotSuffix(repo);
118
+ if (!n) return null;
119
+ return formatKvLines({
120
+ CDP_PORT: 6660 + Number(n),
121
+ WATCHER_PORT: 9010 + Number(n),
122
+ SLOT_ID: `local-extension-${n}`,
123
+ });
124
+ }
125
+
126
+ export function resolveExtensionRuntimePorts(repo) {
127
+ return resolveFarmslotPortsByRepo(repo) ?? resolveDefaultExtensionPorts(repo) ?? '';
128
+ }
129
+
130
+ export function resolveMobileSlotDefaults(repo) {
131
+ const n = inferSlotSuffix(repo);
132
+ if (!n) return null;
133
+ return formatKvLines({
134
+ WATCHER_PORT: 8060 + Number(n),
135
+ IOS_SIMULATOR: `mm-${n}`,
136
+ SLOT_ID: `local-mobile-${n}`,
137
+ });
138
+ }
139
+
140
+ export function resolveMobileRuntimeContext(repo) {
141
+ const ctxPath = path.join(repo, recipeRuntimeDir(), 'agentic-runtime.json');
142
+ if (fs.existsSync(ctxPath)) {
143
+ try {
144
+ const c = JSON.parse(fs.readFileSync(ctxPath, 'utf8'));
145
+ if (c.simulator || c.metroPort) {
146
+ return formatKvLines({
147
+ ...(c.metroPort !== undefined ? { WATCHER_PORT: c.metroPort } : {}),
148
+ ...(c.simulator ? { IOS_SIMULATOR: c.simulator } : {}),
149
+ ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
150
+ });
151
+ }
152
+ } catch {
153
+ /* unreadable context falls through to the provision baseline */
154
+ }
155
+ }
156
+ return resolveMobileProvisionBaseline(repo);
157
+ }
158
+
159
+ // A provisioned-but-unprepared slot has no agentic-runtime.json yet, but the
160
+ // provision baseline records the same authoritative simulator/port identity —
161
+ // without it, a fresh slot's first launch degrades to the simctl `booted`
162
+ // alias and misses the installed dev client entirely.
163
+ function resolveMobileProvisionBaseline(repo) {
164
+ const basePath = path.join(repo, recipeRuntimeDir(), 'runway-provision.json');
165
+ if (!fs.existsSync(basePath)) return null;
166
+ try {
167
+ const c = JSON.parse(fs.readFileSync(basePath, 'utf8'));
168
+ const simulator = c.simulator?.name || c.simulator?.udid;
169
+ if (!simulator && c.watcherPort === undefined) return null;
170
+ return formatKvLines({
171
+ ...(c.watcherPort !== undefined ? { WATCHER_PORT: c.watcherPort } : {}),
172
+ ...(simulator ? { IOS_SIMULATOR: simulator } : {}),
173
+ ...(c.slotId ? { SLOT_ID: c.slotId } : {}),
174
+ });
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+
180
+ export function resolveMobileRuntimePorts(repo) {
181
+ return (
182
+ resolveMobileRuntimeContext(repo)
183
+ ?? resolveFarmslotPortsByRepo(repo)
184
+ ?? resolveMobileSlotDefaults(repo)
185
+ ?? ''
186
+ );
187
+ }
188
+
189
+ export const cliFns = {
190
+ resolve_farmslot_ports_by_repo: resolveFarmslotPortsByRepo,
191
+ resolve_default_extension_ports: resolveDefaultExtensionPorts,
192
+ resolve_extension_runtime_ports: (repo) => resolveExtensionRuntimePorts(repo) || null,
193
+ resolve_mobile_runtime_context: resolveMobileRuntimeContext,
194
+ resolve_mobile_slot_defaults: resolveMobileSlotDefaults,
195
+ resolve_mobile_runtime_ports: (repo) => resolveMobileRuntimePorts(repo) || null,
196
+ };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Node leaf for port resolution — self-contained for runner and injected overlay copies.
3
+
4
+ import { cliFns } from './resolve-farmslot-ports-core.mjs';
5
+
6
+ const [, , fn, repo] = process.argv;
7
+ if (!fn || fn === '-h' || fn === '--help') {
8
+ process.stderr.write('Usage: resolve-farmslot-ports.mjs <fn> <repo>\n');
9
+ process.exit(fn ? 0 : 2);
10
+ }
11
+
12
+ const handler = cliFns[fn];
13
+ if (!handler) {
14
+ process.stderr.write(`resolve-farmslot-ports: unknown fn: ${fn}\n`);
15
+ process.exit(2);
16
+ }
17
+
18
+ const out = handler(repo ?? '.');
19
+ if (!out?.trim()) process.exit(1);
20
+ process.stdout.write(out.endsWith('\n') ? out : `${out}\n`);
@@ -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
+ }
@@ -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,7 +2,13 @@ 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());
@@ -31,31 +37,19 @@ function applyKVLines(output, overwrite) {
31
37
  }
32
38
  }
33
39
  }
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
40
  function resolveMobileSlotPorts(target) {
47
- const ctxOut = sourceResolve("resolve_mobile_runtime_context", target);
48
- if (ctxOut.trim()) {
41
+ const ctxOut = resolveMobileRuntimeContext(target);
42
+ if (ctxOut?.trim()) {
49
43
  applyKVLines(ctxOut, true);
50
44
  return;
51
45
  }
52
- const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
53
- if (poolOut.trim()) {
46
+ const poolOut = resolveFarmslotPortsByRepo(target);
47
+ if (poolOut?.trim()) {
54
48
  applyKVLines(poolOut, true);
55
49
  return;
56
50
  }
57
- const defOut = sourceResolve("resolve_mobile_slot_defaults", target);
58
- if (defOut.trim()) applyKVLines(defOut, false);
51
+ const defOut = resolveMobileSlotDefaults(target);
52
+ if (defOut?.trim()) applyKVLines(defOut, false);
59
53
  }
60
54
  function resolveExtensionSlotPorts(target) {
61
55
  const contextPath = resolveRuntimeContextPath(target);
@@ -70,13 +64,13 @@ function resolveExtensionSlotPorts(target) {
70
64
  process.env["RECIPE_WATCHER_PORT"] = dev;
71
65
  }
72
66
  if (process.env["CDP_PORT"]) return;
73
- const poolOut = sourceResolve("resolve_farmslot_ports_by_repo", target);
74
- if (poolOut.trim()) {
67
+ const poolOut = resolveFarmslotPortsByRepo(target);
68
+ if (poolOut?.trim()) {
75
69
  applyKVLines(poolOut, true);
76
70
  return;
77
71
  }
78
- const defOut = sourceResolve("resolve_default_extension_ports", target);
79
- if (defOut.trim()) applyKVLines(defOut, false);
72
+ const defOut = resolveDefaultExtensionPorts(target);
73
+ if (defOut?.trim()) applyKVLines(defOut, false);
80
74
  }
81
75
  function stopExtensionWatcher(target) {
82
76
  const runtimeAbs = path.join(target, recipeRuntimeDir());
@@ -8,7 +8,7 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
8
8
  // this adapter under the bundled tsx (see commandFor/importsSourceTypescript in
9
9
  // src/live-adapter-contract.ts). The core adapter dynamic-imports the perps
10
10
  // controller TypeScript at runtime, so it MUST run under tsx, not plain node.
11
- import { walletFixturePath } from '../../../../src/index.ts';
11
+ import { walletFixturePath } from '../../harness-exports.mjs';
12
12
 
13
13
  // Shared headless instantiation for the MetaMask `core` adapter.
14
14
  //
@@ -11,7 +11,7 @@ import {
11
11
  recipeHarnessPath,
12
12
  walletFixturePath,
13
13
  captureActiveRecipeRecordingSnapshot,
14
- } from '../../../../src/index.ts';
14
+ } from '../../harness-exports.mjs';
15
15
 
16
16
  // Resolve the Farmslot harness through normal package dependencies by default.
17
17
  // Local Farmslot source is only a dev override handled by src/paths.ts.
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
- import { walletFixturePath } from '../../../../src/index.ts';
3
+ import { walletFixturePath } from '../../harness-exports.mjs';
4
4
 
5
5
  async function fixturePassword(projectRoot) {
6
6
  const candidates = [
@@ -0,0 +1,27 @@
1
+ // Bridge from library actions to the harness path/recording helpers. Published
2
+ // installs load the compiled dist (node cannot type-strip .ts under
3
+ // node_modules); source checkouts without dist fall back to src. Deliberately
4
+ // narrow — importing the package entry here would close an import cycle
5
+ // through src/adapters.ts (which imports these actions) and deadlock.
6
+ const packageRoot = new URL('../../', import.meta.url);
7
+
8
+ async function load(distRelative, srcRelative) {
9
+ try {
10
+ return await import(new URL(distRelative, packageRoot).href);
11
+ } catch {
12
+ return await import(new URL(srcRelative, packageRoot).href);
13
+ }
14
+ }
15
+
16
+ const paths = await load('dist/paths.js', 'src/paths.ts');
17
+ const recording = await load('dist/run-recording.js', 'src/run-recording.ts');
18
+
19
+ export const {
20
+ importRecipeHarnessRuntimeBrowserExtension,
21
+ importRecipeHarnessRuntimeCdp,
22
+ extensionIdPath,
23
+ recipeHarnessPath,
24
+ walletFixturePath,
25
+ } = paths;
26
+
27
+ export const { captureActiveRecipeRecordingSnapshot } = recording;
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { bridgeCommand, runAdapter } from '../platform/bridge.mjs';
3
- import { walletFixturePath } from '../../../../src/index.ts';
3
+ import { walletFixturePath } from '../../harness-exports.mjs';
4
4
 
5
5
  async function fixturePassword(projectRoot) {
6
6
  const candidates = [
@@ -3,7 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { bridgeCommand, bridgeEnv, runAdapter } from '../platform/bridge.mjs';
6
- import { walletFixturePath } from '../../../../src/index.ts';
6
+ import { walletFixturePath } from '../../harness-exports.mjs';
7
7
 
8
8
  async function fixtureProfile(projectRoot) {
9
9
  const candidates = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -201,6 +201,13 @@ export async function decideMobileReadiness(
201
201
  // mid-launch is inverted authority. Surface a teaching block, not an implicit
202
202
  // setup, so a runway preflight never re-does the orchestrator's deps work.
203
203
  if (fast && report.decision === 'install') {
204
+ // A checkout without node_modules cannot even run yarn scripts (yarn exits
205
+ // with "Couldn't find the node_modules state file"), so the taught command
206
+ // must lead with the install when that is the caller's actual state.
207
+ const hasNodeModules = fs.existsSync(path.join(resolved, 'node_modules'));
208
+ const standalone = hasNodeModules
209
+ ? '`yarn setup:expo --no-build-ios --no-build-android`'
210
+ : '`yarn install --immutable && yarn setup:expo --no-build-ios --no-build-android`';
204
211
  return {
205
212
  ...report,
206
213
  decision: 'blocked',
@@ -210,7 +217,7 @@ export async function decideMobileReadiness(
210
217
  ...report.reasons,
211
218
  ],
212
219
  userAction:
213
- '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',
220
+ `run the slot deps/prepare phase; standalone: ${standalone} in the checkout, then re-run \`mm-harness launch <platform>\``,
214
221
  actions: [],
215
222
  };
216
223
  }
@@ -0,0 +1,13 @@
1
+ // TypeScript surface for slot-port resolution — delegates to the self-contained core leaf.
2
+
3
+ export {
4
+ formatKvLines,
5
+ inferSlotSuffix,
6
+ realRepoPath,
7
+ resolveDefaultExtensionPorts,
8
+ resolveExtensionRuntimePorts,
9
+ resolveFarmslotPortsByRepo,
10
+ resolveMobileRuntimeContext,
11
+ resolveMobileRuntimePorts,
12
+ resolveMobileSlotDefaults,
13
+ } from '../../adapters/shared/resolve-farmslot-ports-core.mjs';
@@ -8,7 +8,13 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
 
10
10
  import { readRuntimeContextField, resolveRuntimeContextPath } from '../harness.ts';
11
- import { recipeRuntimeDir, runnerDir } from '../paths.ts';
11
+ import { recipeRuntimeDir } from '../paths.ts';
12
+ import {
13
+ resolveDefaultExtensionPorts,
14
+ resolveFarmslotPortsByRepo,
15
+ resolveMobileRuntimeContext,
16
+ resolveMobileSlotDefaults,
17
+ } from './resolve-farmslot-ports.ts';
12
18
 
13
19
  // Apply KEY=VALUE lines from slot resolution to process.env.
14
20
  // overwrite=true → pool/context match, always overrides existing env.
@@ -42,19 +48,6 @@ export function applyKVLines(output: string, overwrite: boolean): void {
42
48
  }
43
49
  }
44
50
 
45
- function sourceResolve(fn: string, target: string): string {
46
- const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
47
- try {
48
- return execFileSync('bash', ['-c', `source "${resolveScript}" && ${fn} "${target}"`], {
49
- encoding: 'utf8',
50
- timeout: 5000,
51
- stdio: ['ignore', 'pipe', 'ignore'],
52
- });
53
- } catch {
54
- return '';
55
- }
56
- }
57
-
58
51
  // Resolve mobile slot port/simulator: the slot context the orchestrator wrote
59
52
  // into the checkout wins first, then the farmslot pool (both overwrite env),
60
53
  // then the slot-suffix formula (only fills unset vars). Called before explicit
@@ -62,20 +55,20 @@ function sourceResolve(fn: string, target: string): string {
62
55
  export function resolveMobileSlotPorts(target: string): void {
63
56
  // The checkout's own runtime context is authoritative — it names the exact
64
57
  // simulator/port this slot was prepared with, surviving pool renames.
65
- const ctxOut = sourceResolve('resolve_mobile_runtime_context', target);
66
- if (ctxOut.trim()) {
58
+ const ctxOut = resolveMobileRuntimeContext(target);
59
+ if (ctxOut?.trim()) {
67
60
  applyKVLines(ctxOut, true);
68
61
  return;
69
62
  }
70
63
  // Pool match always wins — overwrite whatever is in the environment.
71
- const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
72
- if (poolOut.trim()) {
64
+ const poolOut = resolveFarmslotPortsByRepo(target);
65
+ if (poolOut?.trim()) {
73
66
  applyKVLines(poolOut, true);
74
67
  return;
75
68
  }
76
69
  // Formula match only fills unset vars (never overrides explicit env/pool).
77
- const defOut = sourceResolve('resolve_mobile_slot_defaults', target);
78
- if (defOut.trim()) applyKVLines(defOut, false);
70
+ const defOut = resolveMobileSlotDefaults(target);
71
+ if (defOut?.trim()) applyKVLines(defOut, false);
79
72
  }
80
73
 
81
74
  // Resolve extension slot ports the same way as mobile: the checkout's runtime
@@ -97,13 +90,13 @@ export function resolveExtensionSlotPorts(target: string): void {
97
90
  process.env['RECIPE_WATCHER_PORT'] = dev;
98
91
  }
99
92
  if (process.env['CDP_PORT']) return;
100
- const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
101
- if (poolOut.trim()) {
93
+ const poolOut = resolveFarmslotPortsByRepo(target);
94
+ if (poolOut?.trim()) {
102
95
  applyKVLines(poolOut, true);
103
96
  return;
104
97
  }
105
- const defOut = sourceResolve('resolve_default_extension_ports', target);
106
- if (defOut.trim()) applyKVLines(defOut, false);
98
+ const defOut = resolveDefaultExtensionPorts(target);
99
+ if (defOut?.trim()) applyKVLines(defOut, false);
107
100
  }
108
101
 
109
102
  // Kill the harness-owned webpack watcher for this checkout: pid file first,