@deeeed/metamask-harness 0.3.2 → 0.3.4

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,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.4 - 2026-07-04
4
+
5
+ ### Fixed
6
+ - **IMP-24: published tarball stripped exec bits from shell leaves** — the harness execs `adapters/**/*.sh` leaves directly, but six shipped leaves (`adapters/mobile/lib/metro-listener.sh`, `adapters/shared/activate-repo-node.sh`, `cli-ux.sh`, `harness-path.sh`, `hash-helpers.sh`, `resolve-farmslot-ports.sh`) were committed without the git exec bit (mode `100644`), so npm packed them `644`. A fresh `npm i -g` then hit `EACCES` on the first leaf. All shipped `*.sh` are now stored `100755` in git, so the published tarball packs them executable. Added contract test `tests/contract/packaging-exec-bits.test.sh` — it runs `npm pack` to produce the actual `.tgz`, then asserts via `tar tzvf` that every shipped `*.sh` entry has owner-execute set; fails with the offending path(s) if any is `644`, so this cannot silently regress.
7
+ - **IMP-24: spawn failures are surfaced, not swallowed** — `spawnScript` returned on `result.error` (a leaf that is missing/`ENOENT` or not executable/`EACCES`) before its human-mode stderr forward, so a leaf that could not start produced a silent exit-1 (surfaced upstream as `MOBILE_PREPARE_FAILED`). The `result.error` branch now always writes a teaching diagnostic (`leaf could not start: <leaf> (<errno>)` + a `Next: reinstall mm-harness` hint) to stderr before returning. Covered by `tests/contract/spawn-error-surfaced.test.sh`.
8
+
9
+ ## 0.3.3 - 2026-07-04
10
+
11
+ ### Fixed
12
+ - **IMP-23: per-run mobile runtime dir (`RECIPE_RUNTIME_DIR`)** — every on-disk mobile runtime file (`metro.log`, `metro.pid`, `metro.tmux`, `bridge-status.log`, `wallet-fixture.json`) now resolves from the shared runtime-dir resolver that honors `RECIPE_RUNTIME_DIR` instead of the hard-coded `<target>/temp/recipe/runtime`. `RECIPE_RUNTIME_DIR` must be a non-empty relative path under the target checkout — absolute values are rejected at validation. Shell (`recipe_runtime_dir` in `harness-path.sh`, used by `start-metro.sh`, `wait-for-bridge.sh`, `prewarm-bundle.sh`, and `bridge-runtime/setup-wallet.sh`) and TS (`recipeRuntimePath` in `runtime-decision.ts`) resolve the same location, so a run pointed at an isolated subdir (e.g. `temp/recipe/runtime-8081`) writes and reads nothing under the default. Two harness jobs sharing one checkout each set `RECIPE_RUNTIME_DIR` to a distinct relative subdir for full runtime isolation.
13
+ - **IMP-23: port-scoped Metro guard** — the Metro detect/kill/restart helpers are extracted into `adapters/mobile/lib/metro-listener.sh` with an explicit invariant: discovery, inspection, and signalling key off the managed watcher-port only (`lsof -iTCP:<port> -sTCP:LISTEN`). A Metro listening on any other port is never selected, never has its cmdline read, and is never signalled, so a run managing one port cannot detect or kill a concurrent Metro on another port in the same checkout.
14
+
3
15
  ## 0.3.2 - 2026-07-04
4
16
 
5
17
  ### Fixed
@@ -27,6 +27,15 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
27
  APP_ROOT="${APP_ROOT:-$PWD}"
28
28
  cd "$APP_ROOT"
29
29
 
30
+ # Runtime-dir resolver: wallet-fixture.json lands under the same per-run dir as
31
+ # other mobile runtime files, controlled by RECIPE_RUNTIME_DIR (relative, validated).
32
+ # shellcheck disable=SC1091
33
+ . "$SCRIPT_DIR/../../shared/harness-path.sh"
34
+ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
35
+ echo "setup-wallet: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
36
+ exit 1
37
+ fi
38
+
30
39
  PORT="${WATCHER_PORT:-8081}"
31
40
  [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: WATCHER_PORT must be numeric (got: $PORT)" >&2; exit 1; }
32
41
  SCRIPTS="$SCRIPT_DIR"
@@ -67,11 +76,11 @@ while [[ $# -gt 0 ]]; do
67
76
  done
68
77
 
69
78
  # -- Resolve + validate fixture --
70
- [ -z "$FIXTURE_PATH" ] && FIXTURE_PATH="${WALLET_FIXTURE:-${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/wallet-fixture.json}"
79
+ [ -z "$FIXTURE_PATH" ] && FIXTURE_PATH="${WALLET_FIXTURE:-$(recipe_runtime_dir)/wallet-fixture.json}"
71
80
 
72
81
  if [ ! -f "$FIXTURE_PATH" ]; then
73
82
  echo "ERROR: Fixture not found: $FIXTURE_PATH"
74
- echo " create ${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}/wallet-fixture.json"
83
+ echo " create $(recipe_runtime_dir)/wallet-fixture.json"
75
84
  exit 1
76
85
  fi
77
86
  echo "Reading fixture: $FIXTURE_PATH"
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # metro-listener.sh — port-scoped Metro process helpers.
3
+ #
4
+ # Safety boundary: every function keys off the managed Metro port ($PORT) ONLY.
5
+ # Discovery is `lsof -iTCP:$PORT -sTCP:LISTEN`, so a Metro (or anything) listening
6
+ # on any OTHER port is invisible here — its pid is never in the returned set, its
7
+ # cmdline is never read, and it is never signalled. This is what lets one checkout
8
+ # host concurrent Metro instances on different ports (e.g. 8081 and 8092) while a
9
+ # run that manages 8081 provably cannot detect, restart, or kill the 8092 process.
10
+ #
11
+ # Sourced (not executed) by start-metro.sh and by the contract test. Callers set
12
+ # $PORT (managed watcher-port) and, for metro_listener_valid, $TARGET before use.
13
+
14
+ # metro_ready — true when the managed port serves a running Metro packager status.
15
+ metro_ready() {
16
+ curl -sf --max-time 2 "http://localhost:${PORT}/status" 2>/dev/null | grep -q 'packager-status:running'
17
+ }
18
+
19
+ # metro_listener_pids — pids LISTENing on the managed port (and no other port).
20
+ metro_listener_pids() {
21
+ lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true
22
+ }
23
+
24
+ # metro_listener_valid — 0 when the managed-port listener is this checkout's Metro
25
+ # with the required env; 1 when absent, foreign, or missing env (→ restart). Only
26
+ # ever inspects pids listening on $PORT.
27
+ metro_listener_valid() {
28
+ local pids pid command rv
29
+ pids="$(metro_listener_pids)"
30
+ [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 1
31
+ rv=0
32
+ for pid in $pids; do
33
+ command="$(ps eww -p "$pid" -o command= 2>/dev/null || true)"
34
+ case "$command" in
35
+ *"$TARGET"*) ;;
36
+ *)
37
+ printf 'Metro on port %s not owned by this checkout (pid %s); restarting\n' "$PORT" "$pid" >&2
38
+ rv=1; break
39
+ ;;
40
+ esac
41
+ for required_var in ${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID}; do
42
+ if ! printf '%s\n' "$command" | grep -Eq "(^|[[:space:]])${required_var}="; then
43
+ printf 'Metro on port %s missing required env %s (pid %s); restarting\n' "$PORT" "$required_var" "$pid" >&2
44
+ rv=1; break 2
45
+ fi
46
+ done
47
+ done
48
+ return "$rv"
49
+ }
50
+
51
+ # stop_metro_listener — TERM then KILL only the pids LISTENing on the managed port.
52
+ # A process on any other port is never signalled. 0 when the port is free.
53
+ stop_metro_listener() {
54
+ local pids i
55
+ pids="$(metro_listener_pids)"
56
+ [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 0
57
+ printf 'Stopping Metro listener on port %s: %s\n' "$PORT" "$pids" >&2
58
+ # shellcheck disable=SC2086
59
+ kill $pids 2>/dev/null || true
60
+ for i in $(seq 1 20); do
61
+ pids="$(metro_listener_pids)"
62
+ [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
63
+ sleep 0.25
64
+ done
65
+ printf 'Metro listener still held; force-killing: %s\n' "$pids" >&2
66
+ # shellcheck disable=SC2086
67
+ kill -KILL $pids 2>/dev/null || true
68
+ for i in $(seq 1 20); do
69
+ pids="$(metro_listener_pids)"
70
+ [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
71
+ sleep 0.25
72
+ done
73
+ printf 'start-metro: port %s still occupied after force-kill: %s\n' "$PORT" "$pids" >&2
74
+ return 1
75
+ }
@@ -39,7 +39,15 @@ case "$PLATFORM" in
39
39
  esac
40
40
 
41
41
  TARGET="$(cd "$TARGET" && pwd)"
42
- LOG_DIR="$TARGET/temp/recipe/runtime"
42
+
43
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
44
+ # shellcheck disable=SC1091
45
+ . "$SCRIPT_DIR/../shared/harness-path.sh"
46
+ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
47
+ echo "prewarm-bundle: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
48
+ exit 1
49
+ fi
50
+ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
43
51
  mkdir -p "$LOG_DIR"
44
52
  METRO_LOG="$LOG_DIR/metro.log"
45
53
 
@@ -37,69 +37,28 @@ while [ "$#" -gt 0 ]; do
37
37
  done
38
38
 
39
39
  TARGET="$(cd "$TARGET" && pwd)"
40
- LOG_DIR="$TARGET/temp/recipe/runtime"
40
+
41
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42
+ # shellcheck disable=SC1091
43
+ . "$SCRIPT_DIR/../shared/harness-path.sh"
44
+ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
45
+ echo "start-metro: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
46
+ exit 1
47
+ fi
48
+ # Port-scoped Metro helpers (metro_ready / *_listener_* / stop_metro_listener)
49
+ # only ever inspect or signal a Metro listening on $PORT.
50
+ # shellcheck disable=SC1091
51
+ . "$SCRIPT_DIR/lib/metro-listener.sh"
52
+
53
+ # RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
54
+ # sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
55
+ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
41
56
  mkdir -p "$LOG_DIR"
42
57
  LOG_FILE="$LOG_DIR/metro.log"
43
58
  PID_FILE="$LOG_DIR/metro.pid"
44
59
 
45
60
  # --- helpers ------------------------------------------------------------------
46
61
 
47
- metro_ready() {
48
- curl -sf --max-time 2 "http://localhost:${PORT}/status" 2>/dev/null | grep -q 'packager-status:running'
49
- }
50
-
51
- metro_listener_pids() {
52
- lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN -t 2>/dev/null | tr '\n' ' ' || true
53
- }
54
-
55
- metro_listener_valid() {
56
- local pids pid command rv
57
- pids="$(metro_listener_pids)"
58
- [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 1
59
- rv=0
60
- for pid in $pids; do
61
- command="$(ps eww -p "$pid" -o command= 2>/dev/null || true)"
62
- case "$command" in
63
- *"$TARGET"*) ;;
64
- *)
65
- printf 'Metro on port %s not owned by this checkout (pid %s); restarting\n' "$PORT" "$pid" >&2
66
- rv=1; break
67
- ;;
68
- esac
69
- for required_var in ${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID}; do
70
- if ! printf '%s\n' "$command" | grep -Eq "(^|[[:space:]])${required_var}="; then
71
- printf 'Metro on port %s missing required env %s (pid %s); restarting\n' "$PORT" "$required_var" "$pid" >&2
72
- rv=1; break 2
73
- fi
74
- done
75
- done
76
- return "$rv"
77
- }
78
-
79
- stop_metro_listener() {
80
- local pids i
81
- pids="$(metro_listener_pids)"
82
- [ -n "$(printf '%s' "$pids" | tr -d ' ')" ] || return 0
83
- printf 'Stopping Metro listener on port %s: %s\n' "$PORT" "$pids" >&2
84
- # shellcheck disable=SC2086
85
- kill $pids 2>/dev/null || true
86
- for i in $(seq 1 20); do
87
- pids="$(metro_listener_pids)"
88
- [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
89
- sleep 0.25
90
- done
91
- printf 'Metro listener still held; force-killing: %s\n' "$pids" >&2
92
- # shellcheck disable=SC2086
93
- kill -KILL $pids 2>/dev/null || true
94
- for i in $(seq 1 20); do
95
- pids="$(metro_listener_pids)"
96
- [ -z "$(printf '%s' "$pids" | tr -d ' ')" ] && return 0
97
- sleep 0.25
98
- done
99
- printf 'start-metro: port %s still occupied after force-kill: %s\n' "$PORT" "$pids" >&2
100
- return 1
101
- }
102
-
103
62
  default_metro_workers() {
104
63
  local cores=""
105
64
  if command -v sysctl >/dev/null 2>&1; then
@@ -33,7 +33,15 @@ while [ "$#" -gt 0 ]; do
33
33
  done
34
34
 
35
35
  TARGET="$(cd "$TARGET" && pwd)"
36
- LOG_DIR="$TARGET/temp/recipe/runtime"
36
+
37
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
38
+ # shellcheck disable=SC1091
39
+ . "$SCRIPT_DIR/../shared/harness-path.sh"
40
+ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
41
+ echo "wait-for-bridge: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
42
+ exit 1
43
+ fi
44
+ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
37
45
  mkdir -p "$LOG_DIR"
38
46
 
39
47
  BRIDGE_CJS="$(dirname "$0")/bridge-runtime/cdp-bridge.cjs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -15,6 +15,7 @@ import {
15
15
  } from '@farmslot/recipe-harness/runtime/log-analysis';
16
16
  import { probeMetroPackager, type MetroReachabilityCheck } from '@farmslot/recipe-harness/runtime/metro-probe';
17
17
 
18
+ import { recipeRuntimePath } from '../../paths.ts';
18
19
  import { mobileProductMarkers } from './deps-markers.ts';
19
20
 
20
21
  /**
@@ -62,9 +63,14 @@ const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u
62
63
  const NATIVE_MODULE_STALE =
63
64
  /\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
64
65
 
66
+ // The metro.log location honors RECIPE_RUNTIME_DIR via the shared recipeRuntimePath
67
+ // resolver (relative, validated), so the decision engine reads metro.log from the
68
+ // same per-run dir the mobile leaf scripts write — two jobs sharing one checkout
69
+ // stay isolated. An explicit metroLog override (a --metro-log flag) still wins.
65
70
  function resolveMetroLog(target: string, metroLog?: string): string | null {
66
- const rel = metroLog ?? 'temp/recipe/runtime/metro.log';
67
- const abs = path.isAbsolute(rel) ? rel : path.join(target, rel);
71
+ const abs = metroLog
72
+ ? (path.isAbsolute(metroLog) ? metroLog : path.join(target, metroLog))
73
+ : recipeRuntimePath(target, 'metro.log');
68
74
  return fs.existsSync(abs) ? abs : null;
69
75
  }
70
76
 
@@ -120,7 +120,17 @@ export function spawnScript(
120
120
  maxBuffer: 64 * 1024 * 1024,
121
121
  });
122
122
  if (result.error) {
123
- return { status: 1, output: `script could not start (${path.basename(script)}): ${result.error.message}` };
123
+ // A spawn failure (EACCES on a non-executable leaf, ENOENT on a missing one)
124
+ // is otherwise silent because the human-mode stderr forward below is skipped
125
+ // on early return. Always emit the diagnostic so a leaf that can't start is
126
+ // never a mystery exit-1. For node invocations the leaf is args[0], not node.
127
+ const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
128
+ const code = (result.error as NodeJS.ErrnoException).code ?? 'ESPAWN';
129
+ const message =
130
+ `leaf could not start: ${leaf} (${code})\n` +
131
+ ` Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
132
+ process.stderr.write(`${message}\n`);
133
+ return { status: 1, output: message };
124
134
  }
125
135
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
126
136
  if (!json && output) process.stderr.write(output);