@deeeed/metamask-harness 0.6.2 → 0.7.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 (37) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/adapters/extension/inject.mjs +7 -0
  3. package/adapters/extension/reattach.sh +210 -0
  4. package/adapters/extension/start-watch.sh +11 -17
  5. package/adapters/manifest.json +65 -9
  6. package/adapters/mobile/lib/tmux-viewer.sh +31 -10
  7. package/adapters/mobile/open-device.sh +14 -1
  8. package/adapters/mobile/stop-metro.sh +1 -1
  9. package/adapters/mobile/yarn-setup.sh +15 -1
  10. package/adapters/shared/activate-repo-ruby.sh +124 -0
  11. package/adapters/shared/open-log-window.sh +55 -0
  12. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -205
  13. package/adapters/shared/resolve-farmslot-ports.mjs +4 -19
  14. package/adapters/shared/resolve-farmslot-ports.sh +6 -104
  15. package/adapters/shared/resolve-slot-ports-core.mjs +213 -0
  16. package/adapters/shared/resolve-slot-ports.mjs +20 -0
  17. package/adapters/shared/resolve-slot-ports.sh +110 -0
  18. package/adapters/shared/tmux-session.sh +35 -0
  19. package/dist/adapters/extension/runtime-decision.js +4 -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 +1 -1
  24. package/dist/commands/call.js +5 -0
  25. package/dist/commands/doctor.js +21 -1
  26. package/dist/commands/fixtures.js +5 -3
  27. package/dist/commands/launch/extension.js +80 -6
  28. package/dist/commands/launch/index.js +10 -1
  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 +1 -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 +3 -1
  36. package/dist/paths.js +4 -1
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0
4
+
5
+ The "version people use": every remaining identified fix/improvement batched on the
6
+ 0.6.x stability foundation. Built on top of the extension quick-reuse regression fix
7
+ (0.6.3, below). One themed commit per item; full contract suite green (macOS).
8
+
9
+ ### Added
10
+ - **`call --list` / `run --list`** — one scope-aware listing of everything invocable
11
+ for the detected adapter: the fuzzy short-name action universe (each action's full
12
+ name plus its unambiguous short name — what `call` accepts) and the runnable
13
+ library flows (what `run <flow>` accepts). No `<action>`/`<recipe>` required; same
14
+ shape under `--json`.
15
+ - **`logs --window`** — (re)open the read-only tmux tail window for the resolved
16
+ dev-server port without touching the process (recovers a window closed while
17
+ Metro/webpack keeps running); teaches when there is no run-owned session or log.
18
+ - **`doctor` surfaces Screen-Recording / capture readiness** (macOS) — shells
19
+ `capture-helper doctor` and reports pass/WARN with the grant step; capture is
20
+ optional (absent binary → no row, never a doctor failure). Seam: `CAPTURE_HELPER_PATH`.
21
+
22
+ ### Changed
23
+ - **`fixtures set` is never silent** — an upfront intent line the moment work begins,
24
+ plus the mobile setup-wallet leaf's own progress streamed live (heartbeat) during
25
+ the 20-40s wallet build; stdout stays a clean `--json` envelope.
26
+ - **`actions --action <name>` fuzzy-resolves like `call`** (exact full name → exact
27
+ final segment → substring) and teaches the vocabulary on an unknown name (exit 2,
28
+ `--json` error envelope) instead of throwing.
29
+ - **`run` auto-detects the adapter** from the target/cwd when `--adapter` is absent
30
+ (parity with call/doctor); teaches when the repo type cannot be detected.
31
+ - **Log-tail windows follow a run-owned session ladder** — `RECIPE_TMUX_SESSION` →
32
+ the checkout's `agentic-runtime.json` `session` → the current tmux client — with no
33
+ slot-number derivation (the orchestrator names the session; the harness only
34
+ populates windows). Stale `metro-<port>` windows whose port has no live listener
35
+ are swept on Metro start.
36
+ - **Extension slot ports fill independently** — the resolver no longer early-returns
37
+ once CDP is known, so a context that pins only `cdpPort` still gets `WATCHER_PORT`
38
+ from the pool/formula.
39
+ - Orchestrator-neutral protocol-root env: `METAMASK_RUNNER_PROTOCOL_ROOT` is primary,
40
+ `FARMSLOT_ROOT` a back-compat alias for one release.
41
+
42
+ ### Fixed
43
+ - **Extension `launch` teaches `yarn install --immutable` on missing/stale deps** —
44
+ a pre-watcher deps-freshness probe fails fast (teaching only, never auto-installs)
45
+ instead of letting webpack crash deep on stale `node_modules`; gated so a reusable
46
+ live runtime (reload-in-place) is never blocked.
47
+ - **Core actions run under the target checkout's own tsx** — a core perps action
48
+ dynamically imports the checkout's controller TypeScript, so it now runs under tsx
49
+ (resolved from the target first — the published harness ships none; a thin-installed
50
+ slot has it) rather than bare node throwing `ERR_MODULE_NOT_FOUND`; teaches
51
+ `yarn install` / `yarn build` when no tsx is found.
52
+ - **Repo-pinned Ruby for iOS pod/gem work** — `yarn setup` / the iOS native build
53
+ activate the checkout's `.ruby-version` (asdf → rbenv → mise → PATH) so a cold
54
+ checkout does not build native gems against the system Ruby; teaches the install
55
+ when the pinned Ruby is unresolved.
56
+ - **iOS deep-link dialog pre-approval at provisioning** — the dev-client scheme
57
+ approval is persisted right after app install, so the first headless `openurl` does
58
+ not pause on the "Open in <App>?" dialog (the launch-time write can race).
59
+ - **Extension quick-tier webpack tail window** is regression-guarded (Metro parity).
60
+
61
+ ### Internal
62
+ - Dependabot: no high/critical advisories remain (existing `resolutions` cover them).
63
+ - Boring-library swaps (pidtree, ajv) declined per the "delete hand-rolled code with a
64
+ bug history" bar: the ps/lsof process detection is contract-tested and working, and
65
+ manifest validation is already schema-driven via `@farmslot/protocol`.
66
+ - Orchestrator-neutral port leaf: `resolve-farmslot-ports{,-core}.mjs` / `.sh` and
67
+ `src/adapters/resolve-farmslot-ports.ts` renamed to `resolve-slot-ports*`; the
68
+ function `resolveFarmslotPortsByRepo`, the bash function / cli key
69
+ `resolve_farmslot_ports_by_repo`, and the JS export renamed to the `*slot*`
70
+ equivalents. The old filenames, bash function, cli key, and JS export are kept as
71
+ delegating shims/aliases for ONE RELEASE (deprecation window) — the next release
72
+ removes them. The pool-path constants the leaf reads are left as-is (orchestrator
73
+ data location, not a callable surface).
74
+
75
+ ## 0.6.3
76
+
77
+ ### Fixed
78
+ - **Extension `launch` reuses a healthy live runtime instead of always clean-building** — quick `launch` / `launch --sidepanel` on an extension slot now probes for a reusable runtime (webpack watcher LISTENing on `WATCHER_PORT` + CDP `/json/version` reachable on `CDP_PORT` + dist not stale). When all hold it REATTACHES: re-snapshots `dist/chrome` into the loaded runtime-dist and reloads the extension in place over CDP (service worker + pages), re-asserts the side panel, and applies `--url` — against the existing slot Chrome profile, with no watcher stop, webpack cache clear, or Chrome kill/relaunch. Reuse is best-effort: a failed probe (watcher down, CDP unreachable, stale dist) falls through to the clean path, and `--build` is the explicit escape that always clean-builds + relaunches a fresh runtime (no probe) to force-recover a broken slot.
79
+
3
80
  ## 0.6.2 - 2026-07-06
4
81
 
5
82
  ### Added
@@ -101,6 +101,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(
101
101
  copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
102
102
  copyFile(path.join(runnerDir, 'adapters/extension/seed-fixture.sh'), path.join(harnessDir, 'scripts/seed-fixture.sh'));
103
103
  copyFile(path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh'), path.join(harnessDir, 'scripts/sidepanel-toggle.sh'));
104
+ copyFile(path.join(runnerDir, 'adapters/extension/reattach.sh'), path.join(harnessDir, 'scripts/reattach.sh'));
104
105
  copyFile(path.join(runnerDir, 'adapters/extension/readiness.mjs'), path.join(harnessDir, 'scripts/readiness.mjs'));
105
106
  copyFile(path.join(runnerDir, 'adapters/extension/wallet-fixture-state.cjs'), path.join(harnessDir, 'scripts/wallet-fixture-state.cjs'));
106
107
  copyFile(path.join(runnerDir, 'adapters/extension/verify.sh'), path.join(harnessDir, 'scripts/verify.sh'));
@@ -108,11 +109,16 @@ copyFile(path.join(runnerDir, 'adapters/extension/refresh-build.sh'), path.join(
108
109
  copyFile(path.join(runnerDir, 'adapters/extension/ensure-browser.sh'), path.join(harnessDir, 'scripts/reopen-browser.sh'));
109
110
  copyFile(path.join(runnerDir, 'adapters/shared/harness-path.sh'), path.join(harnessDir, 'scripts/lib/harness-path.sh'));
110
111
  copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.join(harnessDir, 'scripts/lib/activate-repo-node.sh'));
112
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.sh'));
113
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.mjs'));
114
+ copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports-core.mjs'));
115
+ // Back-compat shims (one release) so an overlay consumer using the old leaf names still resolves.
111
116
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
112
117
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
113
118
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
114
119
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
115
120
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
121
+ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
116
122
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
117
123
  makeExecutableTree(path.join(harnessDir, 'scripts'));
118
124
  fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
@@ -138,6 +144,7 @@ const manifest = {
138
144
  refreshBuild: `${harnessRel}/scripts/refresh-build.sh`,
139
145
  reopenBrowser: `${harnessRel}/scripts/reopen-browser.sh`,
140
146
  sidepanelToggle: `${harnessRel}/scripts/sidepanel-toggle.sh`,
147
+ reattach: `${harnessRel}/scripts/reattach.sh`,
141
148
  },
142
149
  patchedFiles: [],
143
150
  backupDir: null,
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env bash
2
+ # reattach.sh — reload a live extension slot in place (no rebuild, no relaunch)
3
+ #
4
+ # Purpose:
5
+ # Quick-reuse leaf for `mm-harness launch` on an already-healthy extension slot:
6
+ # re-snapshots dist/chrome into the loaded runtime-dist so the running Chrome
7
+ # picks up watched code changes, then reloads the extension over CDP (service
8
+ # worker + extension pages) and re-opens the wallet. Optionally navigates a
9
+ # main tab to --start-url. Runs entirely against the EXISTING slot Chrome
10
+ # profile — it never stops the watcher, kills Chrome, or clears the webpack
11
+ # cache (that is the clean path in live.sh).
12
+ #
13
+ # Inputs (flags / env):
14
+ # --target <metamask-extension> (default $PWD; becomes the working dir)
15
+ # --cdp-port <port> (required, numeric) — the slot's CDP endpoint
16
+ # --watcher-port <port> (optional; informational only)
17
+ # --start-url <url> (optional; opened as a main tab beside the wallet)
18
+ # --ext-id <id> (optional; else resolved from the extension.id file or CDP)
19
+ # --dist-dir <rel> (default dist/chrome), --settle-ms <ms> (default 8000)
20
+ #
21
+ # Outputs:
22
+ # Progress on stderr. Exit 0 — reloaded; 1 — repo/reload failed; 2 — bad args
23
+ # / non-numeric port / CDP unreachable.
24
+ #
25
+ # Never touches: product source files; the webpack watcher; the Chrome process;
26
+ # other slots' browsers (single CDP port scope).
27
+ set -euo pipefail
28
+
29
+ TARGET="$PWD"
30
+ CDP_PORT="${CDP_PORT:-}"
31
+ WATCHER_PORT="${WATCHER_PORT:-}"
32
+ START_URL="${EXTENSION_START_URL:-}"
33
+ EXT_ID="${EXT_ID:-}"
34
+ DIST_DIR="dist/chrome"
35
+ SETTLE_MS="8000"
36
+
37
+ require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
38
+ while [ "$#" -gt 0 ]; do
39
+ case "$1" in
40
+ --target) require_value "$@"; TARGET="$2"; shift 2 ;;
41
+ --cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
42
+ --watcher-port) require_value "$@"; WATCHER_PORT="$2"; shift 2 ;;
43
+ --start-url) require_value "$@"; START_URL="$2"; shift 2 ;;
44
+ --ext-id) require_value "$@"; EXT_ID="$2"; shift 2 ;;
45
+ --dist-dir) require_value "$@"; DIST_DIR="$2"; shift 2 ;;
46
+ --settle-ms) require_value "$@"; SETTLE_MS="$2"; shift 2 ;;
47
+ -h|--help)
48
+ echo "Usage: reattach.sh --target <metamask-extension> --cdp-port <port> [--watcher-port <port>] [--start-url <url>] [--ext-id <id>] [--dist-dir <rel>] [--settle-ms <ms>]"
49
+ exit 0
50
+ ;;
51
+ *) echo "reattach: unknown arg: $1" >&2; exit 2 ;;
52
+ esac
53
+ done
54
+
55
+ if [ -z "$CDP_PORT" ]; then
56
+ echo "reattach: --cdp-port is required" >&2
57
+ echo " Next: mm-harness launch --build (clean build + relaunch a fresh slot)" >&2
58
+ exit 2
59
+ fi
60
+ case "$CDP_PORT" in
61
+ *[!0-9]*) echo "reattach: --cdp-port must be numeric (got: $CDP_PORT)" >&2; exit 2 ;;
62
+ esac
63
+
64
+ if [ ! -f "$TARGET/package.json" ]; then
65
+ echo "reattach: --target is not a checkout (no package.json): $TARGET" >&2
66
+ echo " Next: mm-harness launch --target /path/to/metamask-extension --build" >&2
67
+ exit 1
68
+ fi
69
+ TARGET="$(cd "$TARGET" && pwd)"
70
+
71
+ if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
72
+ echo "reattach: CDP not reachable on port ${CDP_PORT}; the slot browser is not live." >&2
73
+ echo " Next: mm-harness launch --build (clean build + relaunch)" >&2
74
+ exit 2
75
+ fi
76
+
77
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
78
+ DIST_ABS="$TARGET/$DIST_DIR"
79
+ # The running Chrome loads the runtime-dist snapshot (frozen at launch), not the
80
+ # live dist. Re-snapshot so the in-place reload serves the latest watched build.
81
+ RUNTIME_DIST_ABS="$TARGET/temp/recipe/runtime/runtime-dist"
82
+
83
+ if [ -f "$SCRIPT_DIR/snapshot-dist.sh" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
84
+ echo "[reattach] re-snapshotting dist -> runtime-dist for in-place reload" >&2
85
+ bash "$SCRIPT_DIR/snapshot-dist.sh" --dist "$DIST_ABS" --runtime-dist "$RUNTIME_DIST_ABS" >&2 || {
86
+ echo "reattach: dist re-snapshot failed; the loaded extension may be mid-rebuild." >&2
87
+ echo " Next: mm-harness launch --build (clean build + relaunch)" >&2
88
+ exit 1
89
+ }
90
+ fi
91
+
92
+ # Resolve the extension id: explicit flag, then the recorded id file, else CDP.
93
+ if [ -z "$EXT_ID" ]; then
94
+ for idf in "$TARGET/temp/recipe/runtime/extension.id" "$SCRIPT_DIR/extension.id"; do
95
+ if [ -f "$idf" ]; then
96
+ EXT_ID="$(tr -d '[:space:]' < "$idf")"
97
+ [ -n "$EXT_ID" ] && break
98
+ fi
99
+ done
100
+ fi
101
+
102
+ cd "$TARGET"
103
+ echo "[reattach] reloading extension in place over CDP :${CDP_PORT}" >&2
104
+
105
+ CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" START_URL="$START_URL" SETTLE_MS="$SETTLE_MS" node <<'NODE'
106
+ let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
107
+
108
+ const port = process.env.CDP_PORT;
109
+ const startUrl = process.env.START_URL || '';
110
+ const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
111
+
112
+ function extensionIdFrom(context, provided) {
113
+ if (provided) return provided;
114
+ const urls = [];
115
+ for (const page of context.pages()) urls.push(page.url());
116
+ for (const worker of context.serviceWorkers()) urls.push(worker.url());
117
+ for (const url of urls) {
118
+ const match = /^chrome-extension:\/\/([^/]+)\//u.exec(url);
119
+ if (match) return match[1];
120
+ }
121
+ return '';
122
+ }
123
+
124
+ async function connect() {
125
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
126
+ const context = browser.contexts()[0];
127
+ return { browser, context };
128
+ }
129
+
130
+ (async () => {
131
+ // Phase 1: trigger a full extension reload (service worker + all pages) from an
132
+ // extension page so Chrome re-reads the freshly snapshotted runtime-dist.
133
+ let { browser, context } = await connect();
134
+ const extId = extensionIdFrom(context, process.env.EXT_ID);
135
+ if (!extId) {
136
+ await browser.close();
137
+ console.error('reattach: could not resolve the extension id from the live CDP targets.');
138
+ console.error(' Next: mm-harness launch --build (clean build + relaunch)');
139
+ process.exit(1);
140
+ }
141
+
142
+ let page = context
143
+ .pages()
144
+ .find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`));
145
+ if (!page) {
146
+ page = await context.newPage();
147
+ await page.goto(`chrome-extension://${extId}/home.html`, {
148
+ waitUntil: 'domcontentloaded',
149
+ timeout: 15000,
150
+ });
151
+ }
152
+ await page.evaluate(() => chrome.runtime.reload()).catch(() => {
153
+ // The reload tears the page context down; a rejected evaluate is expected.
154
+ });
155
+ await browser.close();
156
+
157
+ // Phase 2: wait for the extension to come back, then re-open the wallet and,
158
+ // when requested, a dapp tab. Chrome itself never restarted, so CDP stays up.
159
+ const deadline = Date.now() + settleMs;
160
+ let back = false;
161
+ while (Date.now() < deadline) {
162
+ try {
163
+ const probe = await connect();
164
+ const id = extensionIdFrom(probe.context, extId);
165
+ await probe.browser.close();
166
+ if (id) { back = true; break; }
167
+ } catch {
168
+ // CDP momentarily unavailable during reload; retry until the deadline.
169
+ }
170
+ await new Promise((resolve) => setTimeout(resolve, 250));
171
+ }
172
+ if (!back) {
173
+ console.error(`reattach: extension did not come back within ${settleMs}ms after reload.`);
174
+ console.error(' Next: mm-harness launch --build (clean build + relaunch)');
175
+ process.exit(1);
176
+ }
177
+
178
+ ({ browser, context } = await connect());
179
+ let home = context
180
+ .pages()
181
+ .find(
182
+ (candidate) =>
183
+ candidate.url().startsWith(`chrome-extension://${extId}/`) &&
184
+ !candidate.url().includes('/sidepanel.html'),
185
+ );
186
+ if (!home) {
187
+ home = await context.newPage();
188
+ await home
189
+ .goto(`chrome-extension://${extId}/home.html`, { waitUntil: 'domcontentloaded', timeout: 15000 })
190
+ .catch(() => { /* wallet home is best-effort; the reload already applied */ });
191
+ }
192
+ if (startUrl) {
193
+ const dapp = context
194
+ .pages()
195
+ .find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
196
+ const tab = dapp || (await context.newPage());
197
+ await tab
198
+ .goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 20000 })
199
+ .catch(() => { /* dapp navigation is best-effort */ });
200
+ }
201
+ await browser.close();
202
+ })().catch((error) => {
203
+ console.error(`reattach: ${error && error.message ? error.message : error}`);
204
+ console.error(' Next: mm-harness launch --build (clean build + relaunch)');
205
+ process.exit(1);
206
+ });
207
+ NODE
208
+
209
+ echo "[reattach] extension reloaded in place — no rebuild, no relaunch" >&2
210
+ echo " Next: mm-harness verify (confirm the reloaded runtime)" >&2
@@ -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}"
@@ -178,6 +178,14 @@
178
178
  "inputs": "<status|open|close|toggle|cycle> --cdp-port --ext-id --agent-dir --settle-ms; env REPO, CDP_PORT, EXT_ID, SETTLE_MS, AGENT_DIR",
179
179
  "outputs": "status/progress on stdout; exit 0/1/2"
180
180
  },
181
+ {
182
+ "id": "extension/reattach",
183
+ "entry": "adapters/extension/reattach.sh",
184
+ "kind": "bash",
185
+ "purpose": "Reload a live extension slot in place: re-snapshot dist into the loaded runtime-dist, then reload the extension (service worker + pages) over CDP — no watcher stop, no Chrome kill.",
186
+ "inputs": "--target --cdp-port --watcher-port --start-url --ext-id --dist-dir --settle-ms; env CDP_PORT, WATCHER_PORT, EXTENSION_START_URL, EXT_ID",
187
+ "outputs": "progress on stderr; exit 0/1/2"
188
+ },
181
189
  {
182
190
  "id": "extension/pin-remote-flags",
183
191
  "entry": "adapters/extension/pin-remote-flags.cjs",
@@ -290,6 +298,14 @@
290
298
  "inputs": "json path + dotted field",
291
299
  "outputs": "field value on stdout"
292
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
+ },
293
309
  {
294
310
  "id": "lib/activate-repo-node",
295
311
  "entry": "adapters/shared/activate-repo-node.sh",
@@ -298,6 +314,14 @@
298
314
  "inputs": "activate_repo_node [target-dir]; tries asdf, fnm, mise, nvm, then matching node on PATH",
299
315
  "outputs": "REPO_NODE_BIN, REPO_NODE_PATH_PREFIX; prepends bin dir to PATH; exit 0/1/2"
300
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
+ },
301
325
  {
302
326
  "id": "lib/ensure-runner-deps",
303
327
  "entry": "adapters/shared/ensure-runner-deps.sh",
@@ -306,21 +330,37 @@
306
330
  "inputs": "ensure-runner-deps.sh [RUNNER_DIR]; env FARMSLOT_ROOT / METAMASK_RUNNER_PROTOCOL_ROOT optional local link target",
307
331
  "outputs": "installed node_modules in RUNNER_DIR; exit 0 when deps ready"
308
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
+ },
309
349
  {
310
350
  "id": "lib/resolve-farmslot-ports",
311
351
  "entry": "adapters/shared/resolve-farmslot-ports.mjs",
312
352
  "kind": "node",
313
- "purpose": "Resolve CDP + watcher ports from farmslot pool JSON by repo path, else 6660+N / 9010+N.",
314
- "inputs": "resolve-farmslot-ports.mjs resolve_extension_runtime_ports <repo>",
315
- "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"
316
356
  },
317
357
  {
318
358
  "id": "lib/resolve-farmslot-ports-core",
319
359
  "entry": "adapters/shared/resolve-farmslot-ports-core.mjs",
320
360
  "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"
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)"
324
364
  },
325
365
  {
326
366
  "id": "lib/recipe-harness-root",
@@ -331,13 +371,21 @@
331
371
  "outputs": "recipeHarnessRoot() relative path string"
332
372
  },
333
373
  {
334
- "id": "lib/resolve-farmslot-ports-sh",
335
- "entry": "adapters/shared/resolve-farmslot-ports.sh",
374
+ "id": "lib/resolve-slot-ports-sh",
375
+ "entry": "adapters/shared/resolve-slot-ports.sh",
336
376
  "kind": "lib",
337
- "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.",
338
378
  "inputs": "source + resolve_* / apply_resolved_* function calls",
339
379
  "outputs": "KEY=VALUE lines on stdout; env vars via apply_resolved_*"
340
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
+ },
341
389
  {
342
390
  "id": "lib/reap-checkout-metros",
343
391
  "entry": "adapters/shared/reap-checkout-metros.sh",
@@ -351,6 +399,14 @@
351
399
  "inputs": "--target <checkout> --slot-id <id> --cdp-port <port>",
352
400
  "outputs": "wallet-fixture.json in runtime dir; exit 0 always (non-zero = informational, no source found)"
353
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
+ },
354
410
  {
355
411
  "id": "lib/path-defaults",
356
412
  "entry": "adapters/shared/path-defaults.json",
@@ -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