@deeeed/metamask-harness 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +20 -17
  3. package/{orchestration → adapters}/core/cleanup.sh +2 -2
  4. package/{orchestration → adapters}/core/inject.sh +8 -8
  5. package/{orchestration → adapters}/extension/cleanup.mjs +1 -1
  6. package/{orchestration → adapters}/extension/console-tail.mjs +3 -0
  7. package/{orchestration → adapters}/extension/ensure-browser.sh +4 -4
  8. package/{orchestration → adapters}/extension/inject.mjs +27 -31
  9. package/{orchestration → adapters}/extension/launch.sh +4 -4
  10. package/{orchestration → adapters}/extension/live.sh +5 -5
  11. package/{orchestration → adapters}/extension/readiness.mjs +12 -2
  12. package/{orchestration → adapters}/extension/refresh-build.sh +1 -1
  13. package/{orchestration → adapters}/extension/seed-fixture.sh +1 -1
  14. package/{orchestration → adapters}/extension/start-watch.sh +7 -7
  15. package/{runner → adapters}/extension/verify.sh +11 -11
  16. package/{orchestration → adapters}/extension/wallet-fixture-state.cjs +2 -2
  17. package/{orchestration → adapters}/manifest.json +96 -96
  18. package/{library/actions → adapters}/mobile/bridge-runtime/cdp-bridge.cjs +5 -5
  19. package/{orchestration → adapters}/mobile/cleanup.sh +3 -3
  20. package/{orchestration → adapters}/mobile/inject.sh +22 -24
  21. package/adapters/mobile/open-device.sh +250 -0
  22. package/{orchestration/compat-overlays → adapters/mobile/overlay/compat}/README.md +13 -4
  23. package/adapters/mobile/prewarm-bundle.sh +95 -0
  24. package/adapters/mobile/start-metro.sh +228 -0
  25. package/{runner → adapters}/mobile/verify.sh +6 -6
  26. package/adapters/mobile/wait-for-bridge.sh +108 -0
  27. package/adapters/mobile/yarn-setup.sh +20 -0
  28. package/{orchestration/lib → adapters/shared}/cli-ux.sh +5 -9
  29. package/{orchestration/lib → adapters/shared}/log-tui.mjs +57 -1
  30. package/{orchestration/lib → adapters/shared}/open-debug.mjs +32 -10
  31. package/{orchestration/lib → adapters/shared}/resolve-farmslot-ports.sh +47 -0
  32. package/adapters/shared/sync-wallet-fixture.sh +78 -0
  33. package/bin/mm-harness +5 -6
  34. package/docs/CLI-SPEC.md +41 -2
  35. package/docs/architecture.md +69 -38
  36. package/docs/runtime-file-conventions.md +3 -3
  37. package/library/actions/core/perps/_controller.mjs +1 -1
  38. package/library/actions/extension/platform/cdp.mjs +2 -2
  39. package/library/actions/extension/wallet/ensure_unlocked.mjs +1 -1
  40. package/library/actions/mobile/platform/bridge.mjs +1 -1
  41. package/library/actions/mobile/wallet/ensure_unlocked.mjs +1 -1
  42. package/library/actions/mobile/wallet/setup.mjs +2 -2
  43. package/library/library.json +1 -1
  44. package/package.json +8 -8
  45. package/{orchestration → src/adapters}/extension/runtime-decision.ts +5 -5
  46. package/{orchestration → src/adapters}/extension/runtime.ts +1 -1
  47. package/src/adapters/mobile/prepare.ts +210 -0
  48. package/{orchestration → src/adapters}/mobile/runtime-decision.ts +11 -3
  49. package/{runner/src → src}/adapters.ts +3 -3
  50. package/{orchestration/lib/cli-color.mjs → src/cli-color.ts} +21 -13
  51. package/{orchestration/lib/cli-commands.mjs → src/cli-commands.ts} +52 -45
  52. package/{orchestration/lib/cli-version.mjs → src/cli-version.ts} +35 -17
  53. package/{runner/src → src}/cli.ts +25 -18
  54. package/src/commands/debug.ts +61 -0
  55. package/src/commands/fixtures.ts +196 -0
  56. package/{runner/src → src}/commands/launch.ts +185 -41
  57. package/src/commands/logs.ts +86 -0
  58. package/{runner/src → src}/commands/shared.ts +26 -26
  59. package/{runner/src → src}/harness.ts +97 -10
  60. package/src/index.ts +15 -0
  61. package/{runner/src → src}/live-adapter-contract.ts +1 -1
  62. package/{runner/src → src}/mm-harness-cli.ts +19 -3
  63. package/{runner/src → src}/paths.ts +2 -2
  64. package/{orchestration/lib/progress.mjs → src/progress.ts} +31 -21
  65. package/docs/extension-runtime-commands.md +0 -60
  66. package/docs/harness-cli.md +0 -43
  67. package/orchestration/compat-overlays/mobile/README.md +0 -13
  68. package/orchestration/doctor.mjs +0 -72
  69. package/orchestration/lib/cli-home.mjs +0 -354
  70. package/orchestration/lib/recipe-paths.mjs +0 -26
  71. package/orchestration/mobile/launch.sh +0 -137
  72. package/orchestration/mobile/live.sh +0 -125
  73. package/orchestration/porcelain/metamask-recipe +0 -99
  74. package/orchestration/porcelain/mm-recipe +0 -1591
  75. package/orchestration/porcelain/mme-recipe +0 -1181
  76. package/runner/src/commands/debug.ts +0 -44
  77. package/runner/src/commands/fixtures.ts +0 -99
  78. package/runner/src/commands/logs.ts +0 -60
  79. package/runner/src/index.ts +0 -6
  80. /package/{orchestration → adapters}/extension/launch-browser.cjs +0 -0
  81. /package/{orchestration → adapters}/extension/pin-remote-flags.cjs +0 -0
  82. /package/{orchestration → adapters}/extension/sidepanel-toggle.sh +0 -0
  83. /package/{orchestration → adapters}/extension/snapshot-dist.sh +0 -0
  84. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -0
  85. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/config.cjs +0 -0
  86. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/issue-capture.cjs +0 -0
  87. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/target-discovery.cjs +0 -0
  88. /package/{library/actions → adapters}/mobile/bridge-runtime/lib/ws-client.cjs +0 -0
  89. /package/{library/actions → adapters}/mobile/bridge-runtime/setup-wallet.sh +0 -0
  90. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -0
  91. /package/{library/actions/mobile/app-overlay → adapters/mobile/overlay}/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -0
  92. /package/{orchestration/compat-overlays/mobile → adapters/mobile/overlay/compat}/rn81-message-event-source.patch +0 -0
  93. /package/{orchestration/lib → adapters/shared}/activate-repo-node.sh +0 -0
  94. /package/{orchestration/lib → adapters/shared}/ensure-runner-deps.sh +0 -0
  95. /package/{orchestration/lib → adapters/shared}/harness-path.sh +0 -0
  96. /package/{orchestration/lib → adapters/shared}/hash-helpers.sh +0 -0
  97. /package/{orchestration/lib → adapters/shared}/json-field.sh +0 -0
  98. /package/{orchestration/lib → adapters/shared}/path-defaults.json +0 -0
  99. /package/{orchestration → src/adapters}/extension/ensure-ready.ts +0 -0
  100. /package/{orchestration → src/adapters}/extension/extension-id.ts +0 -0
  101. /package/{orchestration → src/adapters}/mobile/deps-markers.ts +0 -0
  102. /package/{runner/src → src}/completions-cache.ts +0 -0
  103. /package/{runner/src → src}/doctor.ts +0 -0
  104. /package/{runner/src → src}/heal-bounds.ts +0 -0
  105. /package/{runner/src → src}/manifest.ts +0 -0
  106. /package/{runner/src → src}/recording-target.ts +0 -0
  107. /package/{runner/src → src}/run-recording.ts +0 -0
  108. /package/{runner/src → src}/runner.ts +0 -0
  109. /package/{runner/src → src}/types.ts +0 -0
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # wait-for-bridge — poll the CDP bridge until a route target is registered.
3
+ #
4
+ # Calls cdp-bridge.cjs status in a poll loop. Emits structured diagnostics on
5
+ # each probe failure: Metro reachability, React Native target visibility, bundle
6
+ # progress. Fails with non-zero if the bridge is not ready within the poll limit.
7
+ #
8
+ # Inputs:
9
+ # --target <metamask-mobile dir> (default $PWD)
10
+ # --port <number> (default WATCHER_PORT env, else METRO_PORT, else 8081)
11
+ # --max-polls <n> (default MOBILE_BRIDGE_READY_POLLS env, else 90; 2s each = 180s)
12
+ # Outputs:
13
+ # Progress on stderr; exit 0 bridge ready; 1 timeout.
14
+ #
15
+ # Single device op: wait for bridge readiness. Does not start Metro or open the dev client.
16
+ set -uo pipefail
17
+
18
+ TARGET="$PWD"
19
+ PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
20
+ MAX_POLLS="${MOBILE_BRIDGE_READY_POLLS:-90}"
21
+
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ --target) TARGET="$2"; shift 2 ;;
25
+ --port) PORT="$2"; shift 2 ;;
26
+ --max-polls) MAX_POLLS="$2"; shift 2 ;;
27
+ -h|--help)
28
+ printf 'Usage: wait-for-bridge.sh [--target <dir>] [--port <n>] [--max-polls <n>]\n'
29
+ exit 0
30
+ ;;
31
+ *) printf 'wait-for-bridge: unknown arg: %s\n' "$1" >&2; exit 2 ;;
32
+ esac
33
+ done
34
+
35
+ TARGET="$(cd "$TARGET" && pwd)"
36
+ LOG_DIR="$TARGET/temp/recipe/runtime"
37
+ mkdir -p "$LOG_DIR"
38
+
39
+ BRIDGE_CJS="$(dirname "$0")/bridge-runtime/cdp-bridge.cjs"
40
+ STATUS_LOG="$LOG_DIR/bridge-status.log"
41
+ METRO_LOG="$LOG_DIR/metro.log"
42
+
43
+ if [ ! -f "$BRIDGE_CJS" ]; then
44
+ printf 'wait-for-bridge: cdp-bridge.cjs not found at %s\n' "$BRIDGE_CJS" >&2
45
+ exit 1
46
+ fi
47
+
48
+ metro_ready() {
49
+ curl -sf --max-time 2 "http://localhost:${PORT}/status" 2>/dev/null | grep -q 'packager-status:running'
50
+ }
51
+
52
+ bundle_progress() {
53
+ local log="$1"
54
+ [ -f "$log" ] || return 0
55
+ local p
56
+ p="$(grep -E '^[[:space:]]*(iOS|Android).*index\.(js|tsx?).*%|Bundl(ed|ing)|Finished' "$log" 2>/dev/null | tail -1 | tr -d '\r' || true)"
57
+ [ -n "$p" ] && printf ' (%s)' "$p"
58
+ }
59
+
60
+ bridge_wait_reason() {
61
+ if ! metro_ready; then
62
+ printf 'Metro not reachable on port %s' "$PORT"
63
+ return
64
+ fi
65
+ local targets
66
+ targets="$(curl -sf --max-time 2 "http://localhost:${PORT}/json/list" 2>/dev/null || true)"
67
+ if [ "$targets" = "[]" ] || [ -z "$targets" ]; then
68
+ printf 'Metro running; no React Native debug target yet'
69
+ bundle_progress "$METRO_LOG"
70
+ return
71
+ fi
72
+ if [ -s "$STATUS_LOG" ] && grep -qxF '[]' "$STATUS_LOG"; then
73
+ printf 'React Native target visible; in-app bridge not ready yet'
74
+ bundle_progress "$METRO_LOG"
75
+ return
76
+ fi
77
+ printf 'bridge probe failed'
78
+ bundle_progress "$METRO_LOG"
79
+ }
80
+
81
+ bridge_has_route() {
82
+ (cd "$TARGET" && APP_ROOT="$TARGET" node "$BRIDGE_CJS" status > "$STATUS_LOG" 2>&1) \
83
+ && node - "$STATUS_LOG" <<'NODE'
84
+ const fs = require('fs');
85
+ try {
86
+ const value = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
87
+ const targets = Array.isArray(value) ? value : [value];
88
+ if (!targets.some((t) => t && typeof t === 'object' && t.route)) process.exit(1);
89
+ } catch { process.exit(1); }
90
+ NODE
91
+ }
92
+
93
+ for ATTEMPT in $(seq 1 "$MAX_POLLS"); do
94
+ if bridge_has_route; then
95
+ printf 'Mobile bridge ready\n' >&2
96
+ exit 0
97
+ fi
98
+ if [ "$ATTEMPT" = "1" ] || [ $((ATTEMPT % 5)) -eq 0 ]; then
99
+ printf 'Waiting for Mobile bridge (%s/%s): %s\n' "$ATTEMPT" "$MAX_POLLS" "$(bridge_wait_reason)" >&2
100
+ fi
101
+ sleep 2
102
+ done
103
+
104
+ cat "$STATUS_LOG" >&2 || true
105
+ printf 'wait-for-bridge: bridge did not become ready on port %s (%s polls × 2s = %ss)\n' \
106
+ "$PORT" "$MAX_POLLS" "$((MAX_POLLS * 2))" >&2
107
+ printf ' Next: check %s — or run mm-harness app-status\n' "$STATUS_LOG" >&2
108
+ exit 1
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ # yarn-setup — install node_modules for MetaMask Mobile.
3
+ #
4
+ # Inputs: --target <metamask-mobile dir> (default $PWD)
5
+ # Outputs: yarn install output on stderr; exit 0 pass, 1 install failed, 2 bad args.
6
+ #
7
+ # Single device op: install node_modules. No other sequencing.
8
+ set -euo pipefail
9
+
10
+ TARGET="$PWD"
11
+ while [ "$#" -gt 0 ]; do
12
+ case "$1" in
13
+ --target) TARGET="$2"; shift 2 ;;
14
+ -h|--help) printf 'Usage: yarn-setup.sh [--target <dir>]\n'; exit 0 ;;
15
+ *) printf 'yarn-setup: unknown arg: %s\n' "$1" >&2; exit 2 ;;
16
+ esac
17
+ done
18
+
19
+ TARGET="$(cd "$TARGET" && pwd)"
20
+ cd "$TARGET" && exec yarn setup
@@ -26,19 +26,15 @@ recipe_color() {
26
26
  }
27
27
 
28
28
  recipe_progress_script() {
29
- printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/progress.mjs}"
30
- }
31
-
32
- recipe_home_script() {
33
- printf '%s' "${RECIPE_HOME_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-home.mjs}"
29
+ printf '%s' "${RECIPE_PROGRESS_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/progress.ts}"
34
30
  }
35
31
 
36
32
  recipe_log_tui_script() {
37
- printf '%s' "${RECIPE_LOG_TUI_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/log-tui.mjs}"
33
+ printf '%s' "${RECIPE_LOG_TUI_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/adapters/shared/log-tui.mjs}"
38
34
  }
39
35
 
40
36
  recipe_version_script() {
41
- printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/orchestration/lib/cli-version.mjs}"
37
+ printf '%s' "${RECIPE_VERSION_SCRIPT:-${RECIPE_RUNNER_ROOT:?}/src/cli-version.ts}"
42
38
  }
43
39
 
44
40
  recipe_log_ui_mode() {
@@ -79,7 +75,7 @@ recipe_instant_hint() {
79
75
 
80
76
  recipe_progress() {
81
77
  local verb="$1" phase="$2" message="$3" elapsed="${4:-}"
82
- local args=(emit --verb "$verb" --phase "$phase" --message "$message")
78
+ local args=(emit --command "$verb" --phase "$phase" --message "$message")
83
79
  [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
84
80
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
85
81
  node "$(recipe_progress_script)" "${args[@]}" || true
@@ -87,7 +83,7 @@ recipe_progress() {
87
83
 
88
84
  recipe_done() {
89
85
  local verb="$1" status="$2" message="$3" elapsed="${4:-}"
90
- local args=(done --verb "$verb" --status "$status" --message "$message")
86
+ local args=(done --command "$verb" --status "$status" --message "$message")
91
87
  [ -n "$elapsed" ] && args+=(--elapsed-ms "$elapsed")
92
88
  [ "${RECIPE_PROGRESS_JSON:-}" = "1" ] && args+=(--json)
93
89
  node "$(recipe_progress_script)" "${args[@]}" || true
@@ -4,7 +4,63 @@
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import { parseArgs } from 'node:util';
7
- import { color, colorLogEvent } from './cli-color.mjs';
7
+ // Inline color helpers self-contained so both the runner copy and any
8
+ // installed copy work without an external module dependency.
9
+ const STYLES = {
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ label: '\x1b[1;36m',
14
+ cmd: '\x1b[1m',
15
+ ok: '\x1b[1;32m',
16
+ warn: '\x1b[1;33m',
17
+ err: '\x1b[1;31m',
18
+ info: '\x1b[0;37m',
19
+ accent: '\x1b[1;35m',
20
+ path: '\x1b[2m',
21
+ comment: '\x1b[2m',
22
+ active: '\x1b[1m',
23
+ };
24
+
25
+ function colorEnabled(stream = process.stderr) {
26
+ if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '' && process.env.NO_COLOR !== '0') {
27
+ return false;
28
+ }
29
+ if (process.env.RECIPE_NO_COLOR === '1') return false;
30
+ if (process.env.RECIPE_COLOR === '1' || process.env.FORCE_COLOR === '1') return true;
31
+ return Boolean(stream?.isTTY);
32
+ }
33
+
34
+ function color(style, text, { stream = process.stderr } = {}) {
35
+ const value = String(text);
36
+ if (!colorEnabled(stream)) return value;
37
+ const code = STYLES[style];
38
+ if (!code) return value;
39
+ return `${code}${value}${STYLES.reset}`;
40
+ }
41
+
42
+ function classifyLogEvent(event) {
43
+ const text = String(event || '');
44
+ if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
45
+ return 'err';
46
+ }
47
+ if (/compiled successfully|compiled with [0-9]+ warning|PASS runtime-launch|launch pass|verify pass|build complete/iu.test(text)) {
48
+ return 'ok';
49
+ }
50
+ if (/phase [0-9]\/[0-9]|prepare pipeline|snapshot|fixture:|dist-freshness|build-health/iu.test(text)) {
51
+ return 'label';
52
+ }
53
+ if (/webpack [0-9]+%|Bundl(ed|ing)|(iOS|Android).*%/iu.test(text)) {
54
+ return 'warn';
55
+ }
56
+ return 'info';
57
+ }
58
+
59
+ function colorLogEvent(event, { latest = false, stream = process.stderr } = {}) {
60
+ const style = classifyLogEvent(event);
61
+ const painted = color(style === 'label' ? 'label' : style, event, { stream });
62
+ return latest ? color('active', painted, { stream }) : painted;
63
+ }
8
64
 
9
65
  const DEFAULT_EVENTS = 10;
10
66
 
@@ -115,16 +115,34 @@ function openUrl(url) {
115
115
  return null;
116
116
  }
117
117
 
118
+ // CDP-eval fallback: invoke DevSettings native module directly via the bridge
119
+ // when the Metro HTTP endpoint is unreachable or returns a non-OK response.
120
+ // Used for both debug (openDebugger) and dev-menu (show).
121
+ async function tryDevSettingsCdpFallback(numericPort, normalizedAction) {
122
+ const bridgePath = new URL('cdp-bridge.cjs', import.meta.url).pathname;
123
+ const expression =
124
+ normalizedAction === 'dev-menu'
125
+ ? 'NativeModules.DevSettings.show()'
126
+ : 'NativeModules.DevSettings.openDebugger()';
127
+ const result = spawnSync(process.execPath, [bridgePath, 'eval', expression], {
128
+ encoding: 'utf8',
129
+ env: { ...process.env, WATCHER_PORT: String(numericPort) },
130
+ timeout: 15000,
131
+ });
132
+ return result.status === 0;
133
+ }
134
+
118
135
  async function openMobileDebugger(port, action, shouldOpen) {
119
136
  const normalizedAction = action === 'dev-menu' ? 'dev-menu' : 'debug';
120
- const path = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
137
+ const urlPath = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
121
138
  const numericPort = Number.parseInt(String(port), 10);
122
139
  if (!Number.isInteger(numericPort) || numericPort <= 0) {
123
140
  return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
124
141
  }
125
142
 
143
+ let metroError = null;
126
144
  try {
127
- const opened = await fetchOk(`http://127.0.0.1:${numericPort}${path}`, { method: 'POST' });
145
+ const opened = await fetchOk(`http://127.0.0.1:${numericPort}${urlPath}`, { method: 'POST' });
128
146
  if (opened) {
129
147
  return {
130
148
  ok: true,
@@ -135,16 +153,21 @@ async function openMobileDebugger(port, action, shouldOpen) {
135
153
  opened: shouldOpen,
136
154
  };
137
155
  }
156
+ metroError = 'metro-http-nok';
138
157
  } catch (error) {
158
+ metroError = error instanceof Error ? error.message : String(error);
159
+ }
160
+
161
+ // CDP-eval fallback via DevSettings native module.
162
+ const cdpOk = await tryDevSettingsCdpFallback(numericPort, normalizedAction);
163
+ if (cdpOk) {
139
164
  return {
140
- ok: false,
165
+ ok: true,
141
166
  adapter: 'mobile',
142
167
  action: normalizedAction,
143
- method: 'metro-http',
168
+ method: 'cdp-eval',
144
169
  port: numericPort,
145
- error: error instanceof Error ? error.message : String(error),
146
- hint: 'Metro may be down or the debug target is not ready. Try recipe refresh, then recipe runtime-status.',
147
- fallback: 'cdp-eval',
170
+ opened: shouldOpen,
148
171
  };
149
172
  }
150
173
 
@@ -154,9 +177,8 @@ async function openMobileDebugger(port, action, shouldOpen) {
154
177
  action: normalizedAction,
155
178
  method: 'metro-http',
156
179
  port: numericPort,
157
- error: 'metro-http-failed',
158
- hint: 'Metro rejected the open-debugger request. Try recipe refresh, then recipe runtime-status.',
159
- fallback: 'cdp-eval',
180
+ error: metroError,
181
+ hint: 'Metro may be down or the debug target is not ready. Check metro status and try again.',
160
182
  };
161
183
  }
162
184
 
@@ -142,3 +142,50 @@ apply_resolved_extension_ports() {
142
142
  esac
143
143
  done <<< "$resolved"
144
144
  }
145
+
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
+ 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"
156
+ }
157
+
158
+ resolve_mobile_runtime_ports() {
159
+ local repo="${1:-.}"
160
+ if resolve_farmslot_ports_by_repo "$repo"; then
161
+ return 0
162
+ fi
163
+ resolve_mobile_slot_defaults "$repo"
164
+ }
165
+
166
+ apply_resolved_mobile_ports() {
167
+ local repo="${1:-.}" line key val from_pool=false resolved=""
168
+ if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
169
+ from_pool=true
170
+ elif resolved="$(resolve_mobile_slot_defaults "$repo" 2>/dev/null)"; then
171
+ from_pool=false
172
+ else
173
+ return 0
174
+ fi
175
+ while IFS= read -r line; do
176
+ [ -n "$line" ] || continue
177
+ key="${line%%=*}"
178
+ val="${line#*=}"
179
+ case "$key" in
180
+ WATCHER_PORT)
181
+ if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
182
+ ;;
183
+ IOS_SIMULATOR)
184
+ if [ "$from_pool" = true ] || [ -z "${IOS_SIMULATOR:-}" ]; then IOS_SIMULATOR="$val"; fi
185
+ ;;
186
+ SLOT_ID)
187
+ if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
188
+ ;;
189
+ esac
190
+ done <<< "$resolved"
191
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # sync-wallet-fixture.sh — copy the wallet fixture to the canonical runtime slot.
3
+ #
4
+ # Purpose:
5
+ # After a successful overlay install, ensures the canonical wallet fixture is
6
+ # present in the checkout's runtime dir. Non-zero exit is informational (no
7
+ # fixture source found); the install already succeeded, so the overlay is fresh.
8
+ #
9
+ # Inputs (flags / env):
10
+ # --target <checkout> (default $PWD)
11
+ # --slot-id <id> (env FARMSLOT_SLOT_ID, SLOT_ID — farmslot sync source)
12
+ # --cdp-port <port> (env CDP_PORT — extension fixture seeding port)
13
+ #
14
+ # Outputs:
15
+ # Copies wallet-fixture.json to <target>/<RECIPE_RUNTIME_DIR>/wallet-fixture.json
16
+ # when a source is found. Exit 0 always (non-zero only when no source found;
17
+ # callers treat this as informational).
18
+ #
19
+ # OS-glue only: file copy and farmslot delegation; decision logic lives in
20
+ # src/commands/fixtures.ts.
21
+ set -uo pipefail
22
+
23
+ TARGET="$PWD"
24
+ SLOT_ID="${FARMSLOT_SLOT_ID:-${SLOT_ID:-}}"
25
+ CDP_PORT="${CDP_PORT:-}"
26
+
27
+ require_value() { [ "$#" -ge 2 ] || { printf 'Missing value for %s\n' "$1" >&2; exit 2; }; }
28
+
29
+ while [ "$#" -gt 0 ]; do
30
+ case "$1" in
31
+ --target) require_value "$@"; TARGET="$2"; shift 2 ;;
32
+ --slot-id) require_value "$@"; SLOT_ID="$2"; shift 2 ;;
33
+ --cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
34
+ -h|--help)
35
+ printf 'Usage: sync-wallet-fixture.sh [--target <dir>] [--slot-id <id>] [--cdp-port <port>]\n'
36
+ exit 0
37
+ ;;
38
+ *) printf 'Unknown arg: %s\n' "$1" >&2; exit 2 ;;
39
+ esac
40
+ done
41
+
42
+ TARGET="$(cd "$TARGET" && pwd)"
43
+ RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
44
+ CANONICAL="$TARGET/$RUNTIME_DIR/wallet-fixture.json"
45
+
46
+ mkdir -p "$(dirname "$CANONICAL")"
47
+
48
+ # Already present — nothing to do.
49
+ if [ -f "$CANONICAL" ]; then
50
+ exit 0
51
+ fi
52
+
53
+ # RECIPE_WALLET_FIXTURE env points at a fixture directly.
54
+ if [ -n "${RECIPE_WALLET_FIXTURE:-}" ] && [ -f "${RECIPE_WALLET_FIXTURE}" ]; then
55
+ cp "${RECIPE_WALLET_FIXTURE}" "$CANONICAL"
56
+ chmod 600 "$CANONICAL" 2>/dev/null || true
57
+ printf 'sync-wallet-fixture: linked wallet-fixture.json from RECIPE_WALLET_FIXTURE\n' >&2
58
+ exit 0
59
+ fi
60
+
61
+ # Farmslot slot sync (requires farmslot root and sync-fixtures.sh).
62
+ if [ -n "$SLOT_ID" ]; then
63
+ FARMSLOT_ROOT="${FARMSLOT_ROOT:-}"
64
+ for candidate in "$HOME/dev/farmslot" "$HOME/farmslot"; do
65
+ [ -d "$candidate/scripts" ] || continue
66
+ FARMSLOT_ROOT="$candidate"
67
+ break
68
+ done
69
+ if [ -n "$FARMSLOT_ROOT" ] && [ -f "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" ]; then
70
+ if bash "$FARMSLOT_ROOT/scripts/sync-fixtures.sh" --slot "$SLOT_ID" 2>/dev/null; then
71
+ [ -f "$CANONICAL" ] && exit 0
72
+ fi
73
+ fi
74
+ fi
75
+
76
+ # No fixture source found — informational non-zero (callers tolerate this).
77
+ printf 'sync-wallet-fixture: no wallet fixture source found for %s\n' "$TARGET" >&2
78
+ exit 1
package/bin/mm-harness CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # mm-harness — the single front door for the MetaMask recipe loop.
3
3
  #
4
- # Same node/tsx resolution mechanism as bin/metamask-recipe, but the entry is the
5
- # commander-based CLI (runner/src/mm-harness-cli.ts) that presents only the
6
- # validated end-state surface. Legacy names (metamask-recipe/mm-recipe/mme-recipe,
7
- # manifest/live/runtime-*) stay reachable via their own bins until the caller sweep.
4
+ # The entry is the commander-based CLI (src/mm-harness-cli.ts) that presents the
5
+ # end-state surface; it resolves node/tsx/dist the same way whether the package is
6
+ # installed as a dependency or run from a source checkout.
8
7
  set -euo pipefail
9
8
  SOURCE="${BASH_SOURCE[0]}"
10
9
  while [ -L "$SOURCE" ]; do
@@ -19,13 +18,13 @@ done
19
18
  SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd -P)"
20
19
  RUNNER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
21
20
 
22
- ENSURE_DEPS="$RUNNER_DIR/orchestration/lib/ensure-runner-deps.sh"
21
+ ENSURE_DEPS="$RUNNER_DIR/adapters/shared/ensure-runner-deps.sh"
23
22
  if [ -f "$ENSURE_DEPS" ]; then
24
23
  # shellcheck disable=SC1090
25
24
  bash "$ENSURE_DEPS" "$RUNNER_DIR"
26
25
  fi
27
26
 
28
- ENTRY_TS="$RUNNER_DIR/runner/src/mm-harness-cli.ts"
27
+ ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
29
28
  ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
30
29
 
31
30
  find_protocol_root() {
package/docs/CLI-SPEC.md CHANGED
@@ -36,8 +36,8 @@ exhaustive —
36
36
  - **REMOVE** — name/bin/subcommand deleted in the wave; **requires a one-line justification** (needs Arthur's sign-off).
37
37
 
38
38
  **No row may be unmapped.** An unmapped row means the refactor is not allowed to
39
- proceed. Source of truth: the bins' `case` statements + the naming audit in
40
- [harness-cli.md](harness-cli.md). Every row is real.
39
+ proceed. Source of truth: the bins' `case` statements + the naming audit.
40
+ Every row is real.
41
41
 
42
42
  ## Surface A — `metamask-recipe` typed CLI (`runner/src/cli.ts`)
43
43
 
@@ -913,3 +913,42 @@ The fleet is just another agent-grade caller:
913
913
  slot's prepare hook calls the same verb with slot context; dispatch scales it
914
914
  across the fleet.
915
915
 
916
+ ## Appendix — Extension runtime lifecycle (internal notes)
917
+
918
+ Internal implementation notes for the Extension runtime lifecycle — the
919
+ self-healing path that gets a checkout from "installed" to "a live, verified
920
+ extension under CDP control". These are **not** a public command surface: the
921
+ probes below are internal functions, composed for you by `launch --verify` and by
922
+ the readiness commands (`doctor --fix`, `verify`). The runner is the source of
923
+ truth for Extension harness injection, readiness decisions, extension-id
924
+ resolution, and live CDP health checks.
925
+
926
+ Public entry points:
927
+
928
+ - **`install`** writes the Extension runtime overlay under the configured harness
929
+ root. CI/agents run it explicitly; the daily-loop commands auto-ensure it.
930
+ - **`verify`** confirms the overlay is present and healthy — a live CDP health
931
+ probe when a runtime is reachable, no launch.
932
+ - **`launch --verify`** installs the overlay if missing, launches/attaches the
933
+ extension, and polls CDP until the runtime is ready.
934
+ - **`doctor` / `doctor --fix`** report readiness and (with `--fix`) run the
935
+ healing steps below WITHOUT starting the app.
936
+
937
+ Internal lifecycle stages, composed in order (each idempotent, each reporting the
938
+ cheapest next action so the path converges rather than rebuilding from scratch):
939
+
940
+ - **Extension-id resolution** — resolves the deterministic unpacked extension id
941
+ from `dist/chrome/manifest.json`, optionally verified against a live CDP target.
942
+ Runs first; every later stage keys off this id.
943
+ - **Readiness decision** — inspects the checkout + live browser and returns the
944
+ cheapest next action (`install`, `build`, `relaunch`, or `ready`) plus the
945
+ concrete `actions[]` to execute.
946
+ - **Runtime launch** — launches Chrome with the installed harness helper, seeds
947
+ the wallet fixture, and runs live smoke verification. Two tiers: reuse the
948
+ existing dist (quick relaunch), or the clean-build path (clear webpack cache,
949
+ start the harness-owned watcher, wait for a clean compile, then launch + verify).
950
+ - **Convergence** — converges the live browser to exactly one healthy `home.html`
951
+ tab (closing strays, reopening if needed) and confirms it with the health probe.
952
+ - **Health probe** — read-only liveness probe for the running extension over CDP;
953
+ the final gate of convergence and the standalone answer for `verify`.
954
+