@deeeed/metamask-harness 0.6.3 → 0.7.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 (37) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/adapters/core/inject.sh +7 -2
  3. package/adapters/extension/inject.mjs +14 -2
  4. package/adapters/extension/start-watch.sh +11 -17
  5. package/adapters/manifest.json +57 -9
  6. package/adapters/mobile/inject.sh +7 -2
  7. package/adapters/mobile/lib/tmux-viewer.sh +31 -10
  8. package/adapters/mobile/open-device.sh +14 -1
  9. package/adapters/mobile/stop-metro.sh +1 -1
  10. package/adapters/mobile/yarn-setup.sh +15 -1
  11. package/adapters/shared/activate-repo-ruby.sh +124 -0
  12. package/adapters/shared/open-log-window.sh +55 -0
  13. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
  14. package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
  15. package/adapters/shared/resolve-farmslot-ports.sh +6 -104
  16. package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
  17. package/adapters/shared/resolve-slot-ports.mjs +20 -0
  18. package/adapters/shared/resolve-slot-ports.sh +110 -0
  19. package/adapters/shared/tmux-session.sh +35 -0
  20. package/dist/adapters/mobile/provision.js +25 -2
  21. package/dist/adapters/{resolve-farmslot-ports.js → resolve-slot-ports.js} +4 -2
  22. package/dist/adapters/slot-ports.js +8 -10
  23. package/dist/cli-commands.js +3 -3
  24. package/dist/commands/call.js +5 -0
  25. package/dist/commands/doctor.js +55 -2
  26. package/dist/commands/fixtures.js +5 -3
  27. package/dist/commands/launch/extension.js +18 -0
  28. package/dist/commands/launch/index.js +11 -4
  29. package/dist/commands/list-executables.js +48 -0
  30. package/dist/commands/logs.js +25 -2
  31. package/dist/commands/manifest.js +27 -7
  32. package/dist/commands/parse-args.js +2 -0
  33. package/dist/commands/run.js +3 -4
  34. package/dist/live-adapter-contract.js +30 -9
  35. package/dist/mm-harness-cli.js +16 -1
  36. package/dist/paths.js +4 -1
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,87 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Added
6
+ - **`doctor --expect-live`** — exit-coded runtime liveness for hooks and scripts: exit 0 only when the adapter runtime decision is `ready` (extension: watcher + CDP; mobile: metro + bridge; core: deps), non-zero with a teaching escape otherwise. Fails closed on every degraded path; reuses the doctor runtime probe, no duplicated checks.
7
+
8
+ ### Fixed
9
+ - **Per-command `--help` documents every implemented flag** (`call/run --list`, `--library`, `--action-manifest`, `logs --window`, `actions --action`, `launch --url`, `doctor --runtime-dir`, `stop --adapter`) and tab-completion matches — the CLI self-teaches its own surface. A contract test guards help/implementation parity.
10
+ - **Injected runner shims resolve at run time** instead of a path baked at inject time: `MM_HARNESS_BIN` → global `mm-harness` on PATH → the recorded install-time path as last resort → teaching escape. A slot copied to or shared with another machine no longer points at a missing absolute path.
11
+ - Removed two parsed-but-unused `launch` flags (`--yes`, `--json-stream`).
12
+
13
+ ## 0.7.0
14
+
15
+ The "version people use": every remaining identified fix/improvement batched on the
16
+ 0.6.x stability foundation. Built on top of the extension quick-reuse regression fix
17
+ (0.6.3, below). One themed commit per item; full contract suite green (macOS).
18
+
19
+ ### Added
20
+ - **`call --list` / `run --list`** — one scope-aware listing of everything invocable
21
+ for the detected adapter: the fuzzy short-name action universe (each action's full
22
+ name plus its unambiguous short name — what `call` accepts) and the runnable
23
+ library flows (what `run <flow>` accepts). No `<action>`/`<recipe>` required; same
24
+ shape under `--json`.
25
+ - **`logs --window`** — (re)open the read-only tmux tail window for the resolved
26
+ dev-server port without touching the process (recovers a window closed while
27
+ Metro/webpack keeps running); teaches when there is no run-owned session or log.
28
+ - **`doctor` surfaces Screen-Recording / capture readiness** (macOS) — shells
29
+ `capture-helper doctor` and reports pass/WARN with the grant step; capture is
30
+ optional (absent binary → no row, never a doctor failure). Seam: `CAPTURE_HELPER_PATH`.
31
+
32
+ ### Changed
33
+ - **`fixtures set` is never silent** — an upfront intent line the moment work begins,
34
+ plus the mobile setup-wallet leaf's own progress streamed live (heartbeat) during
35
+ the 20-40s wallet build; stdout stays a clean `--json` envelope.
36
+ - **`actions --action <name>` fuzzy-resolves like `call`** (exact full name → exact
37
+ final segment → substring) and teaches the vocabulary on an unknown name (exit 2,
38
+ `--json` error envelope) instead of throwing.
39
+ - **`run` auto-detects the adapter** from the target/cwd when `--adapter` is absent
40
+ (parity with call/doctor); teaches when the repo type cannot be detected.
41
+ - **Log-tail windows follow a run-owned session ladder** — `RECIPE_TMUX_SESSION` →
42
+ the checkout's `agentic-runtime.json` `session` → the current tmux client — with no
43
+ slot-number derivation (the orchestrator names the session; the harness only
44
+ populates windows). Stale `metro-<port>` windows whose port has no live listener
45
+ are swept on Metro start.
46
+ - **Extension slot ports fill independently** — the resolver no longer early-returns
47
+ once CDP is known, so a context that pins only `cdpPort` still gets `WATCHER_PORT`
48
+ from the pool/formula.
49
+ - Orchestrator-neutral protocol-root env: `METAMASK_RUNNER_PROTOCOL_ROOT` is primary,
50
+ `FARMSLOT_ROOT` a back-compat alias for one release.
51
+
52
+ ### Fixed
53
+ - **Extension `launch` teaches `yarn install --immutable` on missing/stale deps** —
54
+ a pre-watcher deps-freshness probe fails fast (teaching only, never auto-installs)
55
+ instead of letting webpack crash deep on stale `node_modules`; gated so a reusable
56
+ live runtime (reload-in-place) is never blocked.
57
+ - **Core actions run under the target checkout's own tsx** — a core perps action
58
+ dynamically imports the checkout's controller TypeScript, so it now runs under tsx
59
+ (resolved from the target first — the published harness ships none; a thin-installed
60
+ slot has it) rather than bare node throwing `ERR_MODULE_NOT_FOUND`; teaches
61
+ `yarn install` / `yarn build` when no tsx is found.
62
+ - **Repo-pinned Ruby for iOS pod/gem work** — `yarn setup` / the iOS native build
63
+ activate the checkout's `.ruby-version` (asdf → rbenv → mise → PATH) so a cold
64
+ checkout does not build native gems against the system Ruby; teaches the install
65
+ when the pinned Ruby is unresolved.
66
+ - **iOS deep-link dialog pre-approval at provisioning** — the dev-client scheme
67
+ approval is persisted right after app install, so the first headless `openurl` does
68
+ not pause on the "Open in <App>?" dialog (the launch-time write can race).
69
+ - **Extension quick-tier webpack tail window** is regression-guarded (Metro parity).
70
+
71
+ ### Internal
72
+ - Dependabot: no high/critical advisories remain (existing `resolutions` cover them).
73
+ - Boring-library swaps (pidtree, ajv) declined per the "delete hand-rolled code with a
74
+ bug history" bar: the ps/lsof process detection is contract-tested and working, and
75
+ manifest validation is already schema-driven via `@farmslot/protocol`.
76
+ - Orchestrator-neutral port leaf: `resolve-farmslot-ports{,-core}.mjs` / `.sh` and
77
+ `src/adapters/resolve-farmslot-ports.ts` renamed to `resolve-slot-ports*`; the
78
+ function `resolveFarmslotPortsByRepo`, the bash function / cli key
79
+ `resolve_farmslot_ports_by_repo`, and the JS export renamed to the `*slot*`
80
+ equivalents. The old filenames, bash function, cli key, and JS export are kept as
81
+ delegating shims/aliases for ONE RELEASE (deprecation window) — the next release
82
+ removes them. The pool-path constants the leaf reads are left as-is (orchestrator
83
+ data location, not a callable surface).
84
+
3
85
  ## 0.6.3
4
86
 
5
87
  ### Fixed
@@ -88,8 +88,13 @@ install_v1_runner_assets() {
88
88
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
89
89
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
90
90
  fi
91
- printf 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ "$MM_HARNESS_BIN" != %s ]; then echo "mm-harness overlay: %s: %s (reinstall to repoint: mm-harness install)" >&2; fi\n' "$runner_exec_q" 'delegating to the runner pinned at install time' "$runner_exec_q"
92
- printf 'exec %s "$@"\n' "$runner_exec_q"
91
+ # Resolve the runner at run time (override > global install > install-time
92
+ # record); the recorded path is last resort so a moved checkout is not pinned.
93
+ printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi'
94
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
95
+ printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$runner_exec_q" "$runner_exec_q"
96
+ printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2'
97
+ printf '%s\n' 'exit 127'
93
98
  } > "$HARNESS_DIR/runner/bin/mm-harness"
94
99
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
95
100
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
@@ -68,12 +68,19 @@ fs.mkdirSync(path.join(harnessDir, 'runner/recipes'), { recursive: true });
68
68
  fs.rmSync(path.join(harnessDir, 'scripts'), { recursive: true, force: true });
69
69
  fs.mkdirSync(path.join(harnessDir, 'scripts/lib'), { recursive: true });
70
70
 
71
+ // Resolve the runner at run time so a moved or shared checkout is never bound to
72
+ // an install-time absolute path: explicit override, then a global install, then
73
+ // the runner recorded at install time as a last resort; teach install on miss.
74
+ const pinnedBin = shellQuote(path.join(runnerDir, 'bin/mm-harness'));
71
75
  const delegate = [
72
76
  '#!/usr/bin/env bash',
73
77
  'set -euo pipefail',
74
78
  protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
75
- `if [ -n "\${MM_HARNESS_BIN:-}" ] && [ "\$MM_HARNESS_BIN" != ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} ]; then echo "mm-harness overlay: delegating to the runner pinned at install time: ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} (reinstall to repoint: mm-harness install)" >&2; fi`,
76
- `exec ${shellQuote(path.join(runnerDir, 'bin/mm-harness'))} "$@"`,
79
+ 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi',
80
+ 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
81
+ `if [ -x ${pinnedBin} ]; then exec ${pinnedBin} "$@"; fi`,
82
+ 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2',
83
+ 'exit 127',
77
84
  ].filter(Boolean).join('\n') + '\n';
78
85
  fs.writeFileSync(path.join(harnessDir, 'runner/bin/mm-harness'), delegate, { mode: 0o755 });
79
86
  if (protocolRoot) fs.writeFileSync(path.join(harnessDir, 'runner/.farmslot-root'), `${protocolRoot}\n`);
@@ -109,11 +116,16 @@ copyFile(path.join(runnerDir, 'adapters/extension/refresh-build.sh'), path.join(
109
116
  copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join(harnessDir, 'scripts/reopen-browser.sh'));
110
117
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
111
118
  copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
119
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.sh'));
120
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.mjs'));
121
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports-core.mjs'));
122
+ // Back-compat shims (one release) so an overlay consumer using the old leaf names still resolves.
112
123
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
113
124
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
114
125
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
115
126
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
116
127
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
128
+ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
117
129
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
118
130
  makeExecutableTree(path.join(harnessDir, 'scripts'));
119
131
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
@@ -174,12 +174,6 @@ write_canonical_watch_pid() {
174
174
  printf '%s\n' "$live_pid" > "$slot_watch_pid_file"
175
175
  }
176
176
 
177
- slot_number() {
178
- local base
179
- base="$(basename "$PWD")"
180
- [[ "$base" =~ -([0-9]+)$ ]] && printf '%s\n' "${BASH_REMATCH[1]}"
181
- }
182
-
183
177
  # Explicit clean-build path: clear the target-scoped webpack cache before
184
178
  # starting the harness-owned watcher. Faster incremental refresh stays on
185
179
  # `mm-harness refresh`; this script is for proving a clean build works.
@@ -240,17 +234,17 @@ path_prefix="${REPO_NODE_PATH_PREFIX:-}"
240
234
  nohup env -u BUNDLED_DEBUGPY_PATH ${path_prefix:+PATH="${path_prefix}${PATH}"} ${WATCHER_PORT:+PORT="$WATCHER_PORT"} yarn start > "$watch_log" 2>&1 &
241
235
  started_pid=$!
242
236
  printf '%s\n' "$started_pid" > "$watch_pid_file"
243
- tmux_session="${RECIPE_TMUX_SESSION:-}"
244
- if [ -z "$tmux_session" ]; then
245
- slot="$(slot_number || true)"
246
- if [ -n "$slot" ] && tmux has-session -t "=mme-$slot" 2>/dev/null; then
247
- tmux_session="mme-$slot"
248
- fi
249
- fi
250
- # Fall back to the session this run is inside: renamed pools and manual runs in
251
- # any tmux still get their watcher window; outside tmux there is nowhere to put one.
252
- if [ -z "$tmux_session" ]; then
253
- tmux_session="$(tmux display-message -p '#S' 2>/dev/null || true)"
237
+ # The orchestrator names the session; the harness only populates windows. Resolve
238
+ # via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session →
239
+ # current tmux client) — no slot-number derivation.
240
+ tmux_session=""
241
+ for _tv_src in "$SCRIPT_DIR/lib/tmux-session.sh" "$SCRIPT_DIR/../shared/tmux-session.sh"; do
242
+ # shellcheck disable=SC1091
243
+ [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
244
+ done
245
+ unset _tv_src
246
+ if command -v resolve_run_tmux_session >/dev/null 2>&1; then
247
+ tmux_session="$(resolve_run_tmux_session "$RUNTIME_DIR")"
254
248
  fi
255
249
  if [ -n "$tmux_session" ] && command -v tmux >/dev/null 2>&1 && tmux has-session -t "$tmux_session" 2>/dev/null; then
256
250
  tmux_window="webpack-${WATCHER_PORT:-default}"
@@ -298,6 +298,14 @@
298
298
  "inputs": "json path + dotted field",
299
299
  "outputs": "field value on stdout"
300
300
  },
301
+ {
302
+ "id": "lib/tmux-session",
303
+ "entry": "adapters/shared/tmux-session.sh",
304
+ "kind": "lib",
305
+ "purpose": "resolve_run_tmux_session: the run-owned tmux session a log-tail window belongs in (RECIPE_TMUX_SESSION -> agentic-runtime.json session -> current client; no slot-number derivation).",
306
+ "inputs": "resolve_run_tmux_session [runtime-dir]; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
307
+ "outputs": "resolved session name on stdout (empty when none)"
308
+ },
301
309
  {
302
310
  "id": "lib/activate-repo-node",
303
311
  "entry": "adapters/shared/activate-repo-node.sh",
@@ -306,6 +314,14 @@
306
314
  "inputs": "activate_repo_node [target-dir]; tries asdf, fnm, mise, nvm, then matching node on PATH",
307
315
  "outputs": "REPO_NODE_BIN, REPO_NODE_PATH_PREFIX; prepends bin dir to PATH; exit 0/1/2"
308
316
  },
317
+ {
318
+ "id": "lib/activate-repo-ruby",
319
+ "entry": "adapters/shared/activate-repo-ruby.sh",
320
+ "kind": "lib",
321
+ "purpose": "Manager-agnostic Ruby pin from .ruby-version / .tool-versions for iOS pod/gem work in non-interactive spawns; best-effort variant warns + teaches when unresolved.",
322
+ "inputs": "activate_repo_ruby [target-dir] (0/1/2); activate_repo_ruby_best_effort [target-dir]; tries asdf, rbenv, mise, then matching ruby on PATH",
323
+ "outputs": "REPO_RUBY_BIN, REPO_RUBY_PATH_PREFIX; prepends bin dir to PATH"
324
+ },
309
325
  {
310
326
  "id": "lib/ensure-runner-deps",
311
327
  "entry": "adapters/shared/ensure-runner-deps.sh",
@@ -314,21 +330,37 @@
314
330
  "inputs": "ensure-runner-deps.sh [RUNNER_DIR]; env FARMSLOT_ROOT / METAMASK_RUNNER_PROTOCOL_ROOT optional local link target",
315
331
  "outputs": "installed node_modules in RUNNER_DIR; exit 0 when deps ready"
316
332
  },
333
+ {
334
+ "id": "lib/resolve-slot-ports",
335
+ "entry": "adapters/shared/resolve-slot-ports.mjs",
336
+ "kind": "node",
337
+ "purpose": "Resolve CDP + watcher ports from the orchestrator's slot-pool JSON by repo path, else 6660+N / 9010+N.",
338
+ "inputs": "resolve-slot-ports.mjs resolve_extension_runtime_ports <repo>",
339
+ "outputs": "CDP_PORT / WATCHER_PORT / SLOT_ID lines on stdout"
340
+ },
341
+ {
342
+ "id": "lib/resolve-slot-ports-core",
343
+ "entry": "adapters/shared/resolve-slot-ports-core.mjs",
344
+ "kind": "lib",
345
+ "purpose": "Self-contained pool/context/formula port resolution imported by the node leaf and TS surface.",
346
+ "inputs": "ESM import from resolve-slot-ports.mjs or resolve-slot-ports.ts",
347
+ "outputs": "resolveSlotPortsByRepo and related functions"
348
+ },
317
349
  {
318
350
  "id": "lib/resolve-farmslot-ports",
319
351
  "entry": "adapters/shared/resolve-farmslot-ports.mjs",
320
352
  "kind": "node",
321
- "purpose": "Resolve CDP + watcher ports from farmslot pool JSON by repo path, else 6660+N / 9010+N.",
322
- "inputs": "resolve-farmslot-ports.mjs resolve_extension_runtime_ports <repo>",
323
- "outputs": "CDP_PORT / WATCHER_PORT / SLOT_ID lines on stdout"
353
+ "purpose": "Back-compat shim (one release): delegates to resolve-slot-ports.mjs.",
354
+ "inputs": "resolve-farmslot-ports.mjs <fn> <repo>",
355
+ "outputs": "same as resolve-slot-ports.mjs"
324
356
  },
325
357
  {
326
358
  "id": "lib/resolve-farmslot-ports-core",
327
359
  "entry": "adapters/shared/resolve-farmslot-ports-core.mjs",
328
360
  "kind": "lib",
329
- "purpose": "Self-contained pool/context/formula port resolution imported by the node leaf and TS surface.",
330
- "inputs": "ESM import from resolve-farmslot-ports.mjs or resolve-farmslot-ports.ts",
331
- "outputs": "resolveFarmslotPortsByRepo and related functions"
361
+ "purpose": "Back-compat shim (one release): re-exports resolve-slot-ports-core.mjs.",
362
+ "inputs": "ESM import of the old path",
363
+ "outputs": "re-exports resolveSlotPortsByRepo (and the resolveFarmslotPortsByRepo alias)"
332
364
  },
333
365
  {
334
366
  "id": "lib/recipe-harness-root",
@@ -339,13 +371,21 @@
339
371
  "outputs": "recipeHarnessRoot() relative path string"
340
372
  },
341
373
  {
342
- "id": "lib/resolve-farmslot-ports-sh",
343
- "entry": "adapters/shared/resolve-farmslot-ports.sh",
374
+ "id": "lib/resolve-slot-ports-sh",
375
+ "entry": "adapters/shared/resolve-slot-ports.sh",
344
376
  "kind": "lib",
345
- "purpose": "Bash-compat sourced wrapper over resolve-farmslot-ports.mjs for stop-metro and legacy callers.",
377
+ "purpose": "Bash-compat sourced wrapper over resolve-slot-ports.mjs for stop-metro and legacy callers.",
346
378
  "inputs": "source + resolve_* / apply_resolved_* function calls",
347
379
  "outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
348
380
  },
381
+ {
382
+ "id": "lib/resolve-farmslot-ports-sh",
383
+ "entry": "adapters/shared/resolve-farmslot-ports.sh",
384
+ "kind": "lib",
385
+ "purpose": "Back-compat shim (one release): sources resolve-slot-ports.sh (keeps the resolve_farmslot_ports_by_repo alias).",
386
+ "inputs": "source",
387
+ "outputs": "same sourced functions as resolve-slot-ports.sh"
388
+ },
349
389
  {
350
390
  "id": "lib/reap-checkout-metros",
351
391
  "entry": "adapters/shared/reap-checkout-metros.sh",
@@ -359,6 +399,14 @@
359
399
  "inputs": "--target <checkout> --slot-id <id> --cdp-port <port>",
360
400
  "outputs": "wallet-fixture.json in runtime dir; exit 0 always (non-zero = informational, no source found)"
361
401
  },
402
+ {
403
+ "id": "lib/open-log-window",
404
+ "entry": "adapters/shared/open-log-window.sh",
405
+ "kind": "bash",
406
+ "purpose": "(Re)open a read-only tmux tail window for a dev-server log in the run-owned session without touching the process (mm-harness logs --window).",
407
+ "inputs": "--window <name> --log <path> [--runtime-dir <dir>]",
408
+ "outputs": "tmux tail window in the resolved session; exit 0 opened / 1 no session or log / 2 bad args"
409
+ },
362
410
  {
363
411
  "id": "lib/path-defaults",
364
412
  "entry": "adapters/shared/path-defaults.json",
@@ -187,8 +187,13 @@ install_v1_runner_assets() {
187
187
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
188
188
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
189
189
  fi
190
- printf 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ "$MM_HARNESS_BIN" != %s ]; then echo "mm-harness overlay: %s: %s (reinstall to repoint: mm-harness install)" >&2; fi\n' "$runner_exec_q" 'delegating to the runner pinned at install time' "$runner_exec_q"
191
- printf 'exec %s "$@"\n' "$runner_exec_q"
190
+ # Resolve the runner at run time (override > global install > install-time
191
+ # record); the recorded path is last resort so a moved checkout is not pinned.
192
+ printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi'
193
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
194
+ printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$runner_exec_q" "$runner_exec_q"
195
+ printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2'
196
+ printf '%s\n' 'exit 127'
192
197
  } > "$HARNESS_DIR/runner/bin/mm-harness"
193
198
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
194
199
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
@@ -13,24 +13,45 @@
13
13
  # Inputs (env/args): $1 = metro log path; PORT, LOG_DIR, and optional `slot` from
14
14
  # the caller; TMUX indicates we are inside a tmux client.
15
15
 
16
+ # Session resolution (RECIPE_TMUX_SESSION → agentic-runtime.json session → current
17
+ # tmux client). Sourced relative to this lib; harness-path fallback for the
18
+ # installed layout where shared libs live beside this file.
19
+ _tv_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
20
+ # shellcheck disable=SC1091
21
+ for _tv_src in "$_tv_dir/tmux-session.sh" "$_tv_dir/../../shared/tmux-session.sh"; do
22
+ [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
23
+ done
24
+ unset _tv_src _tv_dir
25
+
16
26
  # shellcheck disable=SC2329 # sourced by start-metro.sh; also sourced by tests.
17
27
  start_viewer_window() {
18
28
  local metro_log="$1"
19
29
  command -v tmux >/dev/null 2>&1 || return 0
20
30
  local window="metro-${PORT}"
21
31
  local metro_tmux="$LOG_DIR/metro.tmux"
22
- local session=""
23
- # (1) the slot session, when one is named and actually exists. The mm-<slot>
24
- # formula predates the mmdev rename; a session named otherwise simply falls
25
- # through — has-session gates it, so a wrong name never creates a window.
26
- if [ -n "${slot:-}" ] && tmux has-session -t "=mm-${slot}" 2>/dev/null; then
27
- session="mm-${slot}"
28
- # (2) the current session, but ONLY when we are actually inside a tmux client.
29
- elif [ -n "${TMUX:-}" ]; then
30
- session="$(tmux display-message -p '#S' 2>/dev/null || true)"
31
- fi
32
+ # The orchestrator names the session; the harness only populates windows. No
33
+ # slot-number derivation the name comes from the run-owned session ladder.
34
+ local session
35
+ session="$(resolve_run_tmux_session "$LOG_DIR")"
32
36
  # No run-owned session → skip the viewer; no orphaned tail-window is left behind.
33
37
  { [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 0
38
+ # Hygiene: close stale metro-<port> windows in this session whose port has no
39
+ # live listener — a previous run's window left tailing a dead log. The current
40
+ # window is (re)created below, so skip it here.
41
+ if command -v lsof >/dev/null 2>&1; then
42
+ tmux list-windows -t "$session" -F '#{window_name}' 2>/dev/null | while IFS= read -r _win; do
43
+ [ "$_win" = "$window" ] && continue
44
+ case "$_win" in
45
+ metro-*) : ;;
46
+ *) continue ;;
47
+ esac
48
+ _p="${_win#metro-}"
49
+ case "$_p" in ''|*[!0-9]*) continue ;; esac
50
+ if ! lsof -nP -iTCP:"$_p" -sTCP:LISTEN -t >/dev/null 2>&1; then
51
+ tmux kill-window -t "${session}:${_win}" >/dev/null 2>&1 || true
52
+ fi
53
+ done
54
+ fi
34
55
  tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
35
56
  tmux new-window -d -t "$session" -n "$window" "exec tail -n +1 -F $(printf '%q' "$metro_log")"
36
57
  printf '%s:%s\n' "$session" "$window" > "$metro_tmux"
@@ -51,6 +51,15 @@ esac
51
51
 
52
52
  TARGET="$(cd "$TARGET" && pwd)"
53
53
 
54
+ # Pin the repo's Ruby for the iOS native build (pod/gem work): a cold checkout
55
+ # under nohup/tmux otherwise uses the system Ruby and fails native gem builds.
56
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57
+ # shellcheck disable=SC1091
58
+ for _rb in "$SCRIPT_DIR/../shared/activate-repo-ruby.sh" "$SCRIPT_DIR/lib/activate-repo-ruby.sh"; do
59
+ [ -f "$_rb" ] && { . "$_rb"; break; }
60
+ done
61
+ unset _rb
62
+
54
63
  # --- iOS helpers --------------------------------------------------------------
55
64
 
56
65
  ios_app_installed() {
@@ -61,7 +70,11 @@ ios_app_installed() {
61
70
  run_ios_native_build() {
62
71
  local sim_target="$1" reason="$2"
63
72
  printf '%s; installing with yarn start:ios (WATCHER_PORT=%s)\n' "$reason" "$PORT" >&2
64
- (cd "$TARGET" && WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1)
73
+ (
74
+ cd "$TARGET"
75
+ command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
76
+ WATCHER_PORT="$PORT" METRO_PORT="$PORT" IOS_SIMULATOR="$sim_target" EXPO_NO_TYPESCRIPT_SETUP=1 yarn start:ios 2>&1
77
+ )
65
78
  }
66
79
 
67
80
  ensure_ios_app_for_mode() {
@@ -35,7 +35,7 @@ fi
35
35
  # pool/formula fallbacks) so we stop THIS slot's Metro, not the 8081 default.
36
36
  if [ -z "$PORT" ]; then
37
37
  # shellcheck disable=SC1091
38
- . "$SCRIPT_DIR/../shared/resolve-farmslot-ports.sh"
38
+ . "$SCRIPT_DIR/../shared/resolve-slot-ports.sh"
39
39
  resolved_port="$(resolve_mobile_runtime_ports "$TARGET" 2>/dev/null | sed -n 's/^WATCHER_PORT=//p' | head -1 || true)"
40
40
  PORT="${resolved_port:-8081}"
41
41
  fi
@@ -12,6 +12,9 @@
12
12
  # no native). The Metro-only remedy: native builds come from open-device.
13
13
  set -euo pipefail
14
14
 
15
+ # Resolve SCRIPT_DIR before any cd (the script may be invoked by a relative path).
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+
15
18
  TARGET="$PWD"
16
19
  MODE="full"
17
20
  while [ "$#" -gt 0 ]; do
@@ -25,8 +28,19 @@ done
25
28
 
26
29
  TARGET="$(cd "$TARGET" && pwd)"
27
30
  cd "$TARGET"
31
+
32
+ # full mode runs native pods/jetify → pin the repo's Ruby so a cold checkout does
33
+ # not build iOS gems against the system Ruby. Best-effort (never blocks the setup).
34
+ # shellcheck disable=SC1091
35
+ for _rb in "$SCRIPT_DIR/../shared/activate-repo-ruby.sh" "$SCRIPT_DIR/lib/activate-repo-ruby.sh"; do
36
+ [ -f "$_rb" ] && { . "$_rb"; break; }
37
+ done
38
+ unset _rb
39
+
28
40
  case "$MODE" in
29
- full) exec yarn setup ;;
41
+ full)
42
+ command -v activate_repo_ruby_best_effort >/dev/null 2>&1 && activate_repo_ruby_best_effort "$TARGET"
43
+ exec yarn setup ;;
30
44
  expo) exec yarn setup:expo --no-build-ios --no-build-android ;;
31
45
  *) printf 'yarn-setup: unknown --mode: %s (want full|expo)\n' "$MODE" >&2; exit 2 ;;
32
46
  esac
@@ -0,0 +1,124 @@
1
+ # activate-repo-ruby.sh — pin the target repo's declared Ruby for non-interactive spawns
2
+ #
3
+ # iOS pod/gem work (yarn setup, native build) reads Ruby from PATH. Under tmux /
4
+ # nohup / a cold checkout the PATH is the system Ruby, which does not match the
5
+ # repo's .ruby-version and fails `bundle install` / `pod install` with native gem
6
+ # build errors. Sourced by the leaves that trigger pods so they run the pinned
7
+ # Ruby, mirroring activate-repo-node.sh for Node.
8
+ #
9
+ # On success sets REPO_RUBY_VERSION / REPO_RUBY_BIN / REPO_RUBY_BIN_DIR /
10
+ # REPO_RUBY_PATH_PREFIX and prepends the bin dir to PATH.
11
+ #
12
+ # Usage:
13
+ # . /path/to/activate-repo-ruby.sh
14
+ # activate_repo_ruby [target-dir] # 0 pinned; 1 none declared; 2 declared but unresolved
15
+ # activate_repo_ruby_best_effort [dir] # never fails the caller; warns + teaches on 2
16
+
17
+ _repo_ruby_matches_req() {
18
+ local req="$1" actual="${2:-}"
19
+ [ -n "$actual" ] || actual="$("$REPO_RUBY_BIN" -e 'print RUBY_VERSION' 2>/dev/null || true)"
20
+ [ -n "$actual" ] || return 1
21
+ [ "$actual" = "$req" ] && return 0
22
+ case "$req" in
23
+ *.*.*) return 1 ;;
24
+ *.*) [[ "$actual" == "$req."* ]] ;;
25
+ *) [[ "$actual" == "$req"* ]] ;;
26
+ esac
27
+ }
28
+
29
+ _repo_ruby_set_from_bin() {
30
+ local candidate="$1"
31
+ [ -n "$candidate" ] && [ -x "$candidate" ] || return 1
32
+ REPO_RUBY_BIN="$candidate"
33
+ REPO_RUBY_BIN_DIR="$(cd "$(dirname "$REPO_RUBY_BIN")" && pwd)"
34
+ REPO_RUBY_PATH_PREFIX="${REPO_RUBY_BIN_DIR}:"
35
+ export PATH="${REPO_RUBY_BIN_DIR}:${PATH}"
36
+ return 0
37
+ }
38
+
39
+ _repo_ruby_try_req() {
40
+ local target="$1" req="$2"
41
+ [ -n "$req" ] || return 1
42
+ REPO_RUBY_VERSION="$req"
43
+
44
+ if command -v asdf >/dev/null 2>&1; then
45
+ local asdf_root
46
+ asdf_root="$(asdf where ruby "$req" 2>/dev/null || true)"
47
+ if [ -n "$asdf_root" ] && _repo_ruby_set_from_bin "$asdf_root/bin/ruby" && _repo_ruby_matches_req "$req"; then
48
+ echo "[env] ruby $("$REPO_RUBY_BIN" -e 'print RUBY_VERSION') from .ruby-version/.tool-versions via asdf ($req)" >&2
49
+ return 0
50
+ fi
51
+ fi
52
+
53
+ if command -v rbenv >/dev/null 2>&1; then
54
+ local rbenv_root
55
+ rbenv_root="$(rbenv root 2>/dev/null || true)"
56
+ if [ -n "$rbenv_root" ] && _repo_ruby_set_from_bin "$rbenv_root/versions/$req/bin/ruby" && _repo_ruby_matches_req "$req"; then
57
+ echo "[env] ruby $("$REPO_RUBY_BIN" -e 'print RUBY_VERSION') from .ruby-version via rbenv ($req)" >&2
58
+ return 0
59
+ fi
60
+ fi
61
+
62
+ if command -v mise >/dev/null 2>&1; then
63
+ local mise_bin
64
+ mise_bin="$(mise x -C "$target" -- which ruby 2>/dev/null || true)"
65
+ if _repo_ruby_set_from_bin "$mise_bin" && _repo_ruby_matches_req "$req"; then
66
+ echo "[env] ruby $("$REPO_RUBY_BIN" -e 'print RUBY_VERSION') from .ruby-version via mise ($req)" >&2
67
+ return 0
68
+ fi
69
+ fi
70
+ return 1
71
+ }
72
+
73
+ activate_repo_ruby() {
74
+ local target="${1:-.}"
75
+ REPO_RUBY_VERSION=""
76
+ REPO_RUBY_BIN=""
77
+ REPO_RUBY_BIN_DIR=""
78
+ REPO_RUBY_PATH_PREFIX=""
79
+
80
+ local declared=false req=""
81
+ if [ -f "$target/.ruby-version" ]; then
82
+ declared=true
83
+ req="$(tr -d ' \t\r\n' < "$target/.ruby-version")"
84
+ req="${req#ruby-}"
85
+ _repo_ruby_try_req "$target" "$req" && return 0
86
+ fi
87
+ if [ -z "$req" ] && [ -f "$target/.tool-versions" ]; then
88
+ declared=true
89
+ req="$(awk '/^ruby / {print $2; exit}' "$target/.tool-versions")"
90
+ _repo_ruby_try_req "$target" "$req" && return 0
91
+ fi
92
+
93
+ if [ "$declared" != true ]; then
94
+ return 1
95
+ fi
96
+
97
+ # Declared already-on-PATH match (e.g. chruby/system already correct).
98
+ local path_ruby
99
+ path_ruby="$(command -v ruby 2>/dev/null || true)"
100
+ if [ -n "$req" ] && _repo_ruby_set_from_bin "$path_ruby" && _repo_ruby_matches_req "$req"; then
101
+ echo "[env] ruby $("$REPO_RUBY_BIN" -e 'print RUBY_VERSION') already on PATH ($req)" >&2
102
+ return 0
103
+ fi
104
+
105
+ REPO_RUBY_BIN=""
106
+ REPO_RUBY_BIN_DIR=""
107
+ REPO_RUBY_PATH_PREFIX=""
108
+ return 2
109
+ }
110
+
111
+ # Best-effort: never fails the caller. On a declared-but-unresolved Ruby, warn with
112
+ # a teaching hint so a subsequent pod/gem failure is diagnosable (system Ruby).
113
+ activate_repo_ruby_best_effort() {
114
+ local target="${1:-.}"
115
+ # Guard the call so a non-zero return (no version / unresolved) never trips the
116
+ # caller's set -e before we can inspect the code.
117
+ local rc=0
118
+ activate_repo_ruby "$target" || rc=$?
119
+ if [ "$rc" -eq 2 ]; then
120
+ echo "[env] repo pins ruby ${REPO_RUBY_VERSION:-?} (.ruby-version/.tool-versions) but no installed manager provided it; using system ruby." >&2
121
+ echo " Next: install it — asdf install ruby ${REPO_RUBY_VERSION:-<version>} (or rbenv install) — then re-run, so iOS gems build against the pinned Ruby" >&2
122
+ fi
123
+ return 0
124
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env bash
2
+ # open-log-window.sh — (re)open a read-only tmux tail window for a dev-server log
3
+ # in the run-owned session, WITHOUT touching the dev-server process. Idempotent:
4
+ # an existing same-name window is replaced with a fresh tail. Used by
5
+ # `mm-harness logs --window` to recover a window that was closed while Metro/webpack
6
+ # keeps running.
7
+ #
8
+ # Args: --window <name> --log <path> [--runtime-dir <dir>]
9
+ # Exit: 0 opened; 1 no run-owned session / no tmux / log missing (teaches); 2 bad args.
10
+ set -euo pipefail
11
+
12
+ WINDOW=""
13
+ LOG=""
14
+ RUNTIME_DIR=""
15
+ require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
16
+ while [ "$#" -gt 0 ]; do
17
+ case "$1" in
18
+ --window) require_value "$@"; WINDOW="$2"; shift 2 ;;
19
+ --log) require_value "$@"; LOG="$2"; shift 2 ;;
20
+ --runtime-dir) require_value "$@"; RUNTIME_DIR="$2"; shift 2 ;;
21
+ -h|--help) echo "Usage: open-log-window.sh --window <name> --log <path> [--runtime-dir <dir>]"; exit 0 ;;
22
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
23
+ esac
24
+ done
25
+ [ -n "$WINDOW" ] && [ -n "$LOG" ] || { echo "open-log-window: --window and --log are required" >&2; exit 2; }
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ # shellcheck disable=SC1091
29
+ for _src in "$SCRIPT_DIR/tmux-session.sh" "$SCRIPT_DIR/lib/tmux-session.sh"; do
30
+ [ -f "$_src" ] && { . "$_src"; break; }
31
+ done
32
+ unset _src
33
+
34
+ if ! command -v tmux >/dev/null 2>&1; then
35
+ echo "open-log-window: tmux is not installed; cannot open a log window." >&2
36
+ echo " Next: tail the log directly — mm-harness logs --full" >&2
37
+ exit 1
38
+ fi
39
+ if [ ! -f "$LOG" ]; then
40
+ echo "open-log-window: no log at $LOG (dev server not started for this checkout)." >&2
41
+ echo " Next: start it — mm-harness launch" >&2
42
+ exit 1
43
+ fi
44
+ session=""
45
+ command -v resolve_run_tmux_session >/dev/null 2>&1 && session="$(resolve_run_tmux_session "$RUNTIME_DIR")"
46
+ if [ -z "$session" ] || ! tmux has-session -t "=$session" 2>/dev/null; then
47
+ echo "open-log-window: no run-owned tmux session to place the window in." >&2
48
+ echo " Next: run inside your slot's tmux session, or set RECIPE_TMUX_SESSION=<session>, then retry" >&2
49
+ exit 1
50
+ fi
51
+
52
+ tmux kill-window -t "${session}:${WINDOW}" 2>/dev/null || true
53
+ log_q="$(printf '%q' "$LOG")"
54
+ tmux new-window -d -t "$session" -n "$WINDOW" "exec tail -n +1 -F $log_q"
55
+ echo "log window → tmux ${session}:${WINDOW}" >&2