@deeeed/metamask-harness 0.14.2 → 0.14.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
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.14.4 - 2026-07-09
6
+
7
+ ### Fixed
8
+ - Include the new extension viewer cleanup scripts in the adapter manifest so published packages pass adapter-surface validation.
9
+
10
+ ## 0.14.3 - 2026-07-09
11
+
12
+ ### Fixed
13
+ - Extension `status` and `doctor` now accept a sidepanel-only live runtime, matching `run`/`call` health checks instead of requiring a fullscreen `home.html` tab after `launch --sidepanel`.
14
+ - Extension sidepanel launch waits longer for Chrome to expose the `sidepanel.html` CDP target, avoiding false failures after Chrome accepts the open request.
15
+ - Extension tmux viewers are opened only in the run-owned tmux session and are cleaned by `mm-harness stop`, preventing stale `webpack-*`/`console-*` windows from leaking across slots.
16
+ - Extension clean relaunch recognizes prior same-checkout Chrome instances loaded from both timestamped harness snapshots and `temp/recipe/runtime/runtime-dist`, while still refusing foreign CDP owners.
17
+
5
18
  ## 0.14.2 - 2026-07-09
6
19
 
7
20
  ### Fixed
@@ -104,6 +104,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/console-tail.mjs'), path.join(
104
104
  copyFile(path.join(runnerDir, 'adapters/extension/launch.sh'), path.join(harnessDir, 'scripts/launch.sh'));
105
105
  copyFile(path.join(runnerDir, 'adapters/extension/live.sh'), path.join(harnessDir, 'scripts/live.sh'));
106
106
  copyFile(path.join(runnerDir, 'adapters/extension/start-watch.sh'), path.join(harnessDir, 'scripts/start-watch.sh'));
107
+ copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
107
108
  copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
108
109
  copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
109
110
  copyFile(path.join(runnerDir, 'adapters/extension/seed-fixture.sh'), path.join(harnessDir, 'scripts/seed-fixture.sh'));
@@ -126,6 +127,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs')
126
127
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
127
128
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
128
129
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
130
+ copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
129
131
  copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
130
132
  copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
131
133
  makeExecutableTree(path.join(harnessDir, 'scripts'));
@@ -64,7 +64,7 @@ fs.mkdirSync(path.dirname(args['chrome-pid']), { recursive: true });
64
64
  // else is a browser this harness did not launch (possibly the user's personal
65
65
  // Chrome), so we refuse rather than kill it or attach to it.
66
66
  const listenerPids = cdpListenerPids(cdpPort);
67
- const foreignPids = listenerPids.filter((pid) => !processLoadsProfile(pid, args.profile));
67
+ const foreignPids = listenerPids.filter((pid) => !processIsOwnedHarnessBrowser(pid, args.profile, args['extension-dir']));
68
68
  if (foreignPids.length > 0) {
69
69
  throw new Error(
70
70
  `Refusing to launch on CDP port ${cdpPort}: it is held by a browser this harness did not launch (pid ${foreignPids.join(', ')}). ` +
@@ -73,7 +73,7 @@ if (foreignPids.length > 0) {
73
73
  }
74
74
  const ownedPids = new Set(listenerPids);
75
75
  const previousPid = readPidFile(args['chrome-pid']);
76
- if (previousPid !== null && processLoadsProfile(previousPid, args.profile)) ownedPids.add(previousPid);
76
+ if (previousPid !== null && processIsOwnedHarnessBrowser(previousPid, args.profile, args['extension-dir'])) ownedPids.add(previousPid);
77
77
  terminatePids([...ownedPids]);
78
78
  if (args['reset-profile'] !== undefined) {
79
79
  fs.rmSync(args.profile, { recursive: true, force: true });
@@ -164,16 +164,35 @@ function assertIsolatedProfile(profile) {
164
164
  // Proof of ownership: a process is ours iff its command line loads the exact
165
165
  // --user-data-dir we launch with. If the process can't be inspected we treat it
166
166
  // as NOT ours (fail safe — never assume ownership of an opaque process).
167
- function processLoadsProfile(pid, profile) {
168
- if (!Number.isInteger(pid) || pid <= 0) return false;
167
+ function processIsOwnedHarnessBrowser(pid, profile, extensionDir) {
168
+ const command = processCommand(pid);
169
+ if (!command) return false;
170
+ if (command.includes(`--user-data-dir=${profile}`)) return true;
171
+ const ownerRoot = harnessOwnerRootFromExtensionDir(extensionDir);
172
+ return Boolean(ownerRoot && command.includes(`--load-extension=${ownerRoot}`));
173
+ }
174
+
175
+ function harnessOwnerRootFromExtensionDir(extensionDir) {
176
+ const resolved = path.resolve(extensionDir);
177
+ for (const marker of [
178
+ `${path.sep}temp${path.sep}recipe${path.sep}harness${path.sep}extension${path.sep}live${path.sep}`,
179
+ `${path.sep}temp${path.sep}recipe${path.sep}runtime${path.sep}runtime-dist`,
180
+ ]) {
181
+ const markerIndex = resolved.indexOf(marker);
182
+ if (markerIndex !== -1) return resolved.slice(0, markerIndex + marker.length);
183
+ }
184
+ return null;
185
+ }
186
+
187
+ function processCommand(pid) {
188
+ if (!Number.isInteger(pid) || pid <= 0) return null;
169
189
  try {
170
- const command = execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
190
+ return execFileSync('ps', ['-ww', '-o', 'command=', '-p', String(pid)], {
171
191
  encoding: 'utf8',
172
192
  stdio: ['ignore', 'pipe', 'ignore'],
173
193
  });
174
- return command.includes(`--user-data-dir=${profile}`);
175
194
  } catch {
176
- return false;
195
+ return null;
177
196
  }
178
197
  }
179
198
 
@@ -119,21 +119,19 @@ fi
119
119
  # tabs. Skipped silently when there is no tmux session or CDP (headless/CI), so
120
120
  # it never affects the launch result.
121
121
  if [ "$status" = "pass" ] && [ -n "$CDP_PORT" ] && command -v tmux >/dev/null 2>&1; then
122
- console_session="${RECIPE_TMUX_SESSION:-}"
123
- if [ -z "$console_session" ] && [[ "$(basename "$TARGET")" =~ -([0-9]+)$ ]]; then
124
- console_session="mme-${BASH_REMATCH[1]}"
125
- fi
122
+ for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
123
+ # shellcheck disable=SC1091
124
+ [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
125
+ done
126
+ unset _tv_src
126
127
  console_tail="$SCRIPT_DIR/console-tail.mjs"
127
- if [ -n "$console_session" ] && [ -f "$console_tail" ] && tmux has-session -t "$console_session" 2>/dev/null; then
128
+ if command -v tmux_viewer_open >/dev/null 2>&1 && [ -f "$console_tail" ]; then
128
129
  console_runtime="$TARGET/$( (cd "$TARGET" && recipe_runtime_dir) 2>/dev/null || echo temp/recipe/runtime)"
129
130
  mkdir -p "$console_runtime"
130
131
  console_log="$console_runtime/extension-console.log"
131
132
  : > "$console_log"
132
- console_window="console-${CDP_PORT}"
133
- tmux kill-window -t "${console_session}:${console_window}" >/dev/null 2>&1 || true
134
- tmux new-window -d -t "$console_session" -n "$console_window" \
133
+ tmux_viewer_open "console" "$CDP_PORT" "$console_log" "$console_runtime" "$console_runtime/console.tmux" \
135
134
  "exec node $(printf '%q' "$console_tail") --cdp-port ${CDP_PORT} --log $(printf '%q' "$console_log")"
136
- echo "[recipe-harness] extension console viewer in tmux window ${console_session}:${console_window}"
137
135
  fi
138
136
  fi
139
137
 
@@ -19,7 +19,7 @@
19
19
  # bash sidepanel-toggle.sh open --cdp-port 6663 [--ext-id <extension id>]
20
20
  # bash sidepanel-toggle.sh close --cdp-port 6663
21
21
  # bash sidepanel-toggle.sh toggle --cdp-port 6663
22
- # bash sidepanel-toggle.sh cycle --cdp-port 6663 [--settle-ms 4000]
22
+ # bash sidepanel-toggle.sh cycle --cdp-port 6663 [--settle-ms 10000]
23
23
  #
24
24
  # CDP can inspect, activate, and close sidepanel targets. Opening the side panel
25
25
  # is done from a clicked extension-page button so Chrome treats
@@ -66,7 +66,7 @@ RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
66
66
 
67
67
  CDP_PORT="${CDP_PORT:-}"
68
68
  EXT_ID="${EXT_ID:-}"
69
- SETTLE_MS="${SETTLE_MS:-4000}"
69
+ SETTLE_MS="${SETTLE_MS:-10000}"
70
70
  AGENT_DIR="${AGENT_DIR:-$SCRIPT_DIR}"
71
71
 
72
72
  while [[ $# -gt 0 ]]; do
@@ -355,7 +355,7 @@ NODE
355
355
  return 0
356
356
  fi
357
357
 
358
- echo "FAIL: sidepanel did not appear within ${SETTLE_MS}ms" >&2
358
+ echo "FAIL: sidepanel target did not appear within ${SETTLE_MS}ms after Chrome accepted the open request" >&2
359
359
  echo " Current CDP targets:" >&2
360
360
  print_targets >&2
361
361
  exit 3
@@ -237,22 +237,14 @@ printf '%s\n' "$started_pid" > "$watch_pid_file"
237
237
  # The orchestrator names the session; the harness only populates windows. Resolve
238
238
  # via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session →
239
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
240
+ for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
242
241
  # shellcheck disable=SC1091
243
242
  [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
244
243
  done
245
244
  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")"
248
- fi
249
- if [ -n "$tmux_session" ] && command -v tmux >/dev/null 2>&1 && tmux has-session -t "$tmux_session" 2>/dev/null; then
250
- tmux_window="webpack-${WATCHER_PORT:-default}"
251
- while tmux list-windows -t "$tmux_session" -F '#{window_name}' 2>/dev/null | grep -Fxq "$tmux_window"; do
252
- tmux kill-window -t "${tmux_session}:${tmux_window}" 2>/dev/null || break
253
- done
245
+ if command -v tmux_viewer_open >/dev/null 2>&1; then
254
246
  log_q="$(printf '%q' "$watch_log")"
255
- tmux new-window -d -t "$tmux_session" -n "$tmux_window" "exec tail -n +1 -F $log_q"
247
+ tmux_viewer_open "webpack" "${WATCHER_PORT:-default}" "$watch_log" "$RUNTIME_DIR" "$RUNTIME_DIR/webpack.tmux" "exec tail -n +1 -F $log_q"
256
248
  fi
257
249
  ln -sf "$(basename "$watch_log")" "$RUNTIME_DIR/webpack.log"
258
250
 
@@ -277,10 +269,8 @@ for i in {1..240}; do
277
269
  compiled=true
278
270
  break
279
271
  fi
280
- echo '[recipe-harness] compact build watcher exited before compile marker' >&2
281
- [ -n "$LIB_DIR" ] && node "$LIB_DIR/log-tui.mjs" events --log "$watch_log" --events "${RECIPE_LOG_EVENTS:-10}" >&2 || true
282
- rm -f "$watch_pid_file" "$slot_watch_pid_file"
283
- exit 1
272
+ echo '[recipe-harness] compact build watcher exited before compile marker; continuing to poll webpack log' >&2
273
+ watch_tail_pid=""
284
274
  fi
285
275
  if ! write_canonical_watch_pid; then
286
276
  if [ "$i" -le 10 ]; then
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bash
2
+ # stop-viewers.sh — close extension tmux viewers and console tail for one checkout.
3
+ set -euo pipefail
4
+
5
+ TARGET="$PWD"
6
+ while [ "$#" -gt 0 ]; do
7
+ case "$1" in
8
+ --target) TARGET="$2"; shift 2 ;;
9
+ -h|--help) echo "Usage: stop-viewers.sh [--target <metamask-extension>]"; exit 0 ;;
10
+ *) echo "stop-viewers: unknown arg: $1" >&2; exit 2 ;;
11
+ esac
12
+ done
13
+
14
+ TARGET="$(cd "$TARGET" && pwd)"
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ # shellcheck disable=SC1091
17
+ for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
18
+ [ -f "$_hp" ] && { . "$_hp"; break; }
19
+ done
20
+ unset _hp
21
+ if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
22
+ echo "stop-viewers: shared lib harness-path.sh not found; reinstall the runner." >&2
23
+ exit 1
24
+ fi
25
+ # shellcheck disable=SC1091
26
+ for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
27
+ [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
28
+ done
29
+ unset _tv_src
30
+
31
+ runtime_dir="$TARGET/$(recipe_runtime_dir)"
32
+ console_log="$runtime_dir/extension-console.log"
33
+ set +e
34
+
35
+ if command -v tmux_viewer_close_marker >/dev/null 2>&1; then
36
+ tmux_viewer_close_marker "$runtime_dir/webpack.tmux"
37
+ tmux_viewer_close_marker "$runtime_dir/console.tmux"
38
+ fi
39
+ if command -v tmux_viewer_sweep_prefix >/dev/null 2>&1; then
40
+ tmux_viewer_sweep_prefix "webpack" "$runtime_dir"
41
+ tmux_viewer_sweep_prefix "console" "$runtime_dir"
42
+ fi
43
+
44
+ ps -axo pid=,command= 2>/dev/null | while read -r pid command; do
45
+ [ -n "${pid:-}" ] || continue
46
+ case "$command" in
47
+ *console-tail.mjs*) ;;
48
+ *) continue ;;
49
+ esac
50
+ case "$command" in
51
+ *"--log $console_log"*|*"$console_log"*) kill "$pid" 2>/dev/null || true ;;
52
+ esac
53
+ done
54
+
55
+ rm -f "$runtime_dir/console.tmux" "$runtime_dir/webpack.tmux"
56
+ exit 0
@@ -106,6 +106,14 @@
106
106
  "inputs": "--target --runtime-dir --runner-bin --summary",
107
107
  "outputs": "<runtime-dir>/recipe-harness-webpack.{pid,log}; optional summary; exit 0/1/2"
108
108
  },
109
+ {
110
+ "id": "extension/stop-viewers",
111
+ "entry": "adapters/extension/stop-viewers.sh",
112
+ "kind": "bash",
113
+ "purpose": "Close extension webpack/console tmux viewers and the checkout-scoped console tail.",
114
+ "inputs": "--target <metamask-extension>",
115
+ "outputs": "removes viewer markers and closes matching run-owned windows; exit 0/1/2"
116
+ },
109
117
  {
110
118
  "id": "extension/console-tail",
111
119
  "entry": "adapters/extension/console-tail.mjs",
@@ -306,6 +314,14 @@
306
314
  "inputs": "resolve_run_tmux_session [runtime-dir]; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
307
315
  "outputs": "resolved session name on stdout (empty when none)"
308
316
  },
317
+ {
318
+ "id": "lib/tmux-viewer",
319
+ "entry": "adapters/shared/tmux-viewer.sh",
320
+ "kind": "lib",
321
+ "purpose": "Run-owned tmux log viewer helpers that open, replace, and clean prefixed viewer windows without slot-name inference.",
322
+ "inputs": "tmux_viewer_open/close_marker/sweep_prefix functions; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
323
+ "outputs": "viewer windows and marker files; no-op when no run-owned session is available"
324
+ },
309
325
  {
310
326
  "id": "lib/activate-repo-node",
311
327
  "entry": "adapters/shared/activate-repo-node.sh",
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env bash
2
+ # tmux-viewer.sh — run-owned tmux log viewer helpers.
3
+ #
4
+ # The orchestrator owns session names; the harness only opens windows inside a
5
+ # session resolved by tmux-session.sh. When no run-owned session is available the
6
+ # viewer is skipped. Runtime processes still run and write logs.
7
+
8
+ _tv_shared_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ # shellcheck disable=SC1091
10
+ for _tv_src in "$_tv_shared_dir/tmux-session.sh" "$_tv_shared_dir/lib/tmux-session.sh"; do
11
+ [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
12
+ done
13
+ unset _tv_src _tv_shared_dir
14
+
15
+ tmux_viewer_port_is_listening() {
16
+ local port="${1:-}"
17
+ case "$port" in ''|*[!0-9]*) return 1 ;; esac
18
+ command -v lsof >/dev/null 2>&1 || return 1
19
+ lsof -nP -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1
20
+ }
21
+
22
+ tmux_viewer_open() {
23
+ local prefix="$1" port="$2" log="$3" runtime_dir="$4" marker_file="$5" command="$6"
24
+ command -v tmux >/dev/null 2>&1 || return 0
25
+ command -v resolve_run_tmux_session >/dev/null 2>&1 || return 0
26
+
27
+ local session window
28
+ session="$(resolve_run_tmux_session "$runtime_dir")"
29
+ { [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 0
30
+
31
+ window="${prefix}-${port:-default}"
32
+ tmux list-windows -t "$session" -F '#{window_name}' 2>/dev/null | while IFS= read -r _win; do
33
+ case "$_win" in
34
+ "$prefix"-*) ;;
35
+ *) continue ;;
36
+ esac
37
+ _port="${_win#"$prefix"-}"
38
+ if [ "$_win" != "$window" ] || ! tmux_viewer_port_is_listening "$_port"; then
39
+ tmux kill-window -t "${session}:${_win}" >/dev/null 2>&1 || true
40
+ fi
41
+ done
42
+
43
+ tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
44
+ tmux new-window -d -t "$session" -n "$window" "$command"
45
+ if [ -n "$marker_file" ]; then
46
+ mkdir -p "$(dirname "$marker_file")"
47
+ printf '%s:%s\n' "$session" "$window" > "$marker_file"
48
+ fi
49
+ printf '%s log viewer → tmux %s:%s\n' "$prefix" "$session" "$window" >&2
50
+ }
51
+
52
+ tmux_viewer_close_marker() {
53
+ local marker_file="$1"
54
+ command -v tmux >/dev/null 2>&1 || { rm -f "$marker_file"; return 0; }
55
+ if [ -f "$marker_file" ]; then
56
+ local target
57
+ target="$(cat "$marker_file" 2>/dev/null || true)"
58
+ [ -z "$target" ] || tmux kill-window -t "$target" >/dev/null 2>&1 || true
59
+ fi
60
+ rm -f "$marker_file"
61
+ }
62
+
63
+ tmux_viewer_sweep_prefix() {
64
+ local prefix="$1" runtime_dir="$2"
65
+ command -v tmux >/dev/null 2>&1 || return 0
66
+ command -v resolve_run_tmux_session >/dev/null 2>&1 || return 0
67
+ local session
68
+ session="$(resolve_run_tmux_session "$runtime_dir")"
69
+ { [ -n "$session" ] && tmux has-session -t "=$session" 2>/dev/null; } || return 0
70
+ tmux list-windows -t "$session" -F '#{window_name}' 2>/dev/null | while IFS= read -r _win; do
71
+ case "$_win" in
72
+ "$prefix"-*) tmux kill-window -t "${session}:${_win}" >/dev/null 2>&1 || true ;;
73
+ esac
74
+ done
75
+ }
@@ -184,11 +184,11 @@ function distCheck(target) {
184
184
  function isExtensionDistStale(target) {
185
185
  return distCheck(path.resolve(target)).status === "stale";
186
186
  }
187
- async function cdpCheck(target, cdpPort) {
187
+ async function cdpCheck(target, cdpPort, pageMode) {
188
188
  if (!cdpPort) return { status: "skipped" };
189
189
  try {
190
190
  const { checkExtensionRuntimeHealth } = await import("./runtime.js");
191
- const report = await checkExtensionRuntimeHealth(target, cdpPort);
191
+ const report = await checkExtensionRuntimeHealth(target, cdpPort, { pageMode });
192
192
  return report.status === "PASS" ? { status: "pass" } : { status: "fail", findings: report.findings };
193
193
  } catch (error) {
194
194
  return { status: "fail", findings: [error instanceof Error ? error.message : String(error)] };
@@ -206,7 +206,7 @@ async function decideExtensionReadiness(target, options = {}) {
206
206
  const webpackCache = webpackCacheCheck(resolved);
207
207
  const buildLog = buildLogCheck(resolved, options.watchLog);
208
208
  const dist = distCheck(resolved);
209
- const cdp = await cdpCheck(resolved, options.cdpPort);
209
+ const cdp = await cdpCheck(resolved, options.cdpPort, options.pageMode);
210
210
  const checks = { deps, webpackCache, buildLog, dist, cdp };
211
211
  const install = [{ id: "yarn-install", argv: ["yarn", "install", "--immutable"], cwd: resolved }];
212
212
  const relaunch = [{ id: "relaunch-browser" }];
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
- import { recipeRuntimePath } from "../../paths.js";
3
+ import { recipeRuntimePath, runnerDir } from "../../paths.js";
4
4
  import { resolveExtensionSlotPorts, stopExtensionWatcher } from "../slot-ports.js";
5
5
  import { decideExtensionReadiness } from "./runtime-decision.js";
6
6
  function watcherStatus(buildLog) {
@@ -16,7 +16,7 @@ const extensionSurface = {
16
16
  },
17
17
  async runtimeStatus(target) {
18
18
  const cdpPort = process.env.CDP_PORT ? parseInt(process.env.CDP_PORT, 10) : void 0;
19
- const report = await decideExtensionReadiness(target, { cdpPort });
19
+ const report = await decideExtensionReadiness(target, { cdpPort, pageMode: "home-or-sidepanel" });
20
20
  return {
21
21
  decision: report.decision,
22
22
  reasonCode: report.reasonCode,
@@ -47,8 +47,8 @@ const extensionSurface = {
47
47
  describe: () => "webpack watcher",
48
48
  stop(target) {
49
49
  const signalled = stopExtensionWatcher(target);
50
- const port = process.env.WATCHER_PORT ?? "default";
51
- spawnSync("tmux", ["kill-window", "-t", `webpack-${port}`], { stdio: "ignore", timeout: 2e3 });
50
+ const stopViewers = path.join(runnerDir, "adapters/extension/stop-viewers.sh");
51
+ spawnSync("bash", [stopViewers, "--target", target], { stdio: "ignore", timeout: 5e3 });
52
52
  const summary = signalled > 0 ? `stopped webpack watcher (${signalled} process${signalled === 1 ? "" : "es"}) for ${target}` : `webpack watcher not running for ${target} \u2014 nothing to stop`;
53
53
  return { kind: "stopped", status: 0, summary, signalled };
54
54
  }
@@ -58,8 +58,21 @@ async function handleStatus({ options }) {
58
58
  )
59
59
  );
60
60
  } else {
61
+ const runtime = !fast && adapter === "extension" ? await surface.runtimeStatus(target) : void 0;
61
62
  console.log(
62
- JSON.stringify({ schemaVersion: 1, command: "status", adapter, target, devices, next }, null, 2)
63
+ JSON.stringify(
64
+ {
65
+ schemaVersion: 1,
66
+ command: "status",
67
+ adapter,
68
+ target,
69
+ devices,
70
+ ...runtime ? { runtime } : {},
71
+ next: runtime ? nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime) : next
72
+ },
73
+ null,
74
+ 2
75
+ )
63
76
  );
64
77
  }
65
78
  return EXIT.ok;
@@ -73,11 +86,28 @@ async function handleStatus({ options }) {
73
86
  renderMobileLiveBlock(devices, liveView.liveMap, out);
74
87
  renderAndroidPortHints(liveView.androidPortHints, out);
75
88
  console.log(`${out("label", "Next:")} ${out("cmd", nextForLive(next, liveView.liveMap))}`);
89
+ } else if (!fast && adapter === "extension") {
90
+ const runtime = await surface.runtimeStatus(target);
91
+ renderRuntimeStatus(runtime, out);
92
+ console.log(`${out("label", "Next:")} ${out("cmd", nextForRuntime(surface.hints.launch, surface.hints.relaunch, runtime))}`);
76
93
  } else {
77
94
  console.log(`${out("label", "Next:")} ${out("cmd", next)}`);
78
95
  }
79
96
  return EXIT.ok;
80
97
  }
98
+ function renderRuntimeStatus(runtime, out) {
99
+ const decisionStyle = runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
100
+ const deps = runtime.deps ? ` deps=${out(runtime.deps === "current" ? "ok" : "warn", runtime.deps)}` : "";
101
+ const devServer = runtime.devServer ? ` ${runtime.devServer.label}=${out(runtime.devServer.status === "up" ? "ok" : "warn", runtime.devServer.status)}` : "";
102
+ console.log(
103
+ `${out("label", "runtime:")} decision=${out(decisionStyle, runtime.decision)}${runtime.reasonCode ? ` ${out("dim", `(${runtime.reasonCode})`)}` : ""}${deps}${devServer}`
104
+ );
105
+ }
106
+ function nextForRuntime(launchHint, relaunchHint, runtime) {
107
+ if (runtime.decision === "ready") return "mm-harness logs";
108
+ if (runtime.decision === "relaunch") return launchHint;
109
+ return relaunchHint;
110
+ }
81
111
  export {
82
112
  handleStatus
83
113
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"