@deeeed/metamask-harness 0.2.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.
- package/CHANGELOG.md +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ensure-browser.sh — ensure the slot's Playwright Chromium is running
|
|
3
|
+
# (auto-detects the slot, kills any existing browser by recorded PID,
|
|
4
|
+
# relaunches against the existing dist/profile/port; requires an
|
|
5
|
+
# already-prepared profile). Project-specific (MetaMask Extension).
|
|
6
|
+
#
|
|
7
|
+
# Purpose:
|
|
8
|
+
# Restarts the slot's Chromium against the existing profile + dist build,
|
|
9
|
+
# re-resolves the extension id via the installed runner, and unlocks.
|
|
10
|
+
#
|
|
11
|
+
# Inputs (flags / env):
|
|
12
|
+
# --slot-id <id> (default <host>-<repo-basename>), --repo <checkout>,
|
|
13
|
+
# --cdp-port <port> (auto-detected from browser.pid when omitted),
|
|
14
|
+
# --runtime-dir <dir>, --watcher-port <port>; env CHROME_USER_DATA_DIR,
|
|
15
|
+
# RECIPE_HARNESS_ROOT. Repo/runtime-dir auto-detected by walking up from
|
|
16
|
+
# browser.pid/webpack.pid when omitted.
|
|
17
|
+
#
|
|
18
|
+
# Outputs:
|
|
19
|
+
# <runtime-dir>/{browser.pid,chromium.pid,extension.id}; progress on stdout.
|
|
20
|
+
# Exit 0 — browser reopened and ready; 1 — preflight/launch failure.
|
|
21
|
+
#
|
|
22
|
+
# Never touches: other slots' browsers (kills by recorded PID/profile only);
|
|
23
|
+
# the dist build (read-only).
|
|
24
|
+
#
|
|
25
|
+
# Usage (from worker template — all vars are template-expanded):
|
|
26
|
+
# bash <project>/setup/reopen-browser.sh \
|
|
27
|
+
# --slot-id macwork-metamask-extension-5 \
|
|
28
|
+
# --repo /Users/deeeed/dev/metamask/metamask-extension-5 \
|
|
29
|
+
# --cdp-port 6665 \
|
|
30
|
+
# --runtime-dir <runtime-dir>
|
|
31
|
+
#
|
|
32
|
+
# Requires: webpack watch running, valid build in dist/chrome.
|
|
33
|
+
set -euo pipefail
|
|
34
|
+
|
|
35
|
+
# --- Parse args ---
|
|
36
|
+
SLOT_ID=""
|
|
37
|
+
REPO=""
|
|
38
|
+
CDP_PORT=""
|
|
39
|
+
RUNTIME_DIR=""
|
|
40
|
+
WATCHER_PORT=""
|
|
41
|
+
|
|
42
|
+
while [[ $# -gt 0 ]]; do
|
|
43
|
+
case "$1" in
|
|
44
|
+
--slot-id) SLOT_ID="$2"; shift 2 ;;
|
|
45
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
46
|
+
--cdp-port) CDP_PORT="$2"; shift 2 ;;
|
|
47
|
+
--runtime-dir) RUNTIME_DIR="$2"; shift 2 ;;
|
|
48
|
+
--watcher-port) WATCHER_PORT="$2"; shift 2 ;;
|
|
49
|
+
-h|--help)
|
|
50
|
+
echo "Usage: ensure-browser.sh [--slot-id <id>] [--repo <metamask-extension>] [--cdp-port <port>] [--runtime-dir <dir>] [--watcher-port <port>]"
|
|
51
|
+
exit 0
|
|
52
|
+
;;
|
|
53
|
+
*) echo "Unknown flag: $1" >&2; exit 1 ;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
# --- Auto-detect from script location or CWD ---
|
|
58
|
+
# Script lives at <repo>/<runtime_dir>/reopen-browser.sh
|
|
59
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
60
|
+
# shellcheck disable=SC1091
|
|
61
|
+
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
|
|
62
|
+
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
63
|
+
done
|
|
64
|
+
unset _hp
|
|
65
|
+
if ! command -v harness_root >/dev/null 2>&1; then
|
|
66
|
+
echo "reopen-browser: shared lib orchestration/lib/harness-path.sh missing; reinstall the harness." >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
if [[ -z "$RUNTIME_DIR" ]]; then
|
|
70
|
+
# Infer runtime_dir as relative path from repo root
|
|
71
|
+
# Script is at <repo>/<runtime-dir>/reopen-browser.sh, so runtime_dir is inferred from that relative path
|
|
72
|
+
if [[ -f "$SCRIPT_DIR/browser.pid" || -f "$SCRIPT_DIR/webpack.pid" || -f "$SCRIPT_DIR/wallet-fixture.json" ]]; then
|
|
73
|
+
AGENT_DIR_ABS="$SCRIPT_DIR"
|
|
74
|
+
elif [[ -f "$PWD/browser.pid" || -f "$PWD/webpack.pid" ]]; then
|
|
75
|
+
AGENT_DIR_ABS="$PWD"
|
|
76
|
+
fi
|
|
77
|
+
if [[ -n "${AGENT_DIR_ABS:-}" ]]; then
|
|
78
|
+
# Walk up to find repo root (has package.json + dist/chrome)
|
|
79
|
+
d="$AGENT_DIR_ABS"
|
|
80
|
+
while [[ "$d" != "/" ]]; do
|
|
81
|
+
if [[ -f "$d/package.json" && -d "$d/dist/chrome" ]]; then
|
|
82
|
+
REPO="${REPO:-$d}"
|
|
83
|
+
# runtime_dir is the relative path from repo to agent dir
|
|
84
|
+
RUNTIME_DIR="${AGENT_DIR_ABS#$d/}"
|
|
85
|
+
break
|
|
86
|
+
fi
|
|
87
|
+
d="$(dirname "$d")"
|
|
88
|
+
done
|
|
89
|
+
fi
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
[[ -z "$REPO" ]] && { echo "Error: cannot detect repo — pass --repo" >&2; exit 1; }
|
|
93
|
+
RUNTIME_DIR="${RUNTIME_DIR:-$(recipe_runtime_dir)}"
|
|
94
|
+
|
|
95
|
+
# Auto-detect slot-id from directory name (e.g. metamask-extension-6 → macwork-metamask-extension-6)
|
|
96
|
+
if [[ -z "$SLOT_ID" ]]; then
|
|
97
|
+
REPO_BASENAME="$(basename "$REPO")"
|
|
98
|
+
HOSTNAME_SHORT="$(hostname -s)"
|
|
99
|
+
SLOT_ID="${HOSTNAME_SHORT}-${REPO_BASENAME}"
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# Auto-detect CDP port from existing browser launch args or agent files
|
|
103
|
+
if [[ -z "$CDP_PORT" && -f "${REPO}/${RUNTIME_DIR}/browser.pid" ]]; then
|
|
104
|
+
BPID=$(cat "${REPO}/${RUNTIME_DIR}/browser.pid" 2>/dev/null)
|
|
105
|
+
if [[ -n "$BPID" ]]; then
|
|
106
|
+
CDP_PORT=$(ps -p "$BPID" -o args= 2>/dev/null | grep -oE 'remote-debugging-port=[0-9]+' | cut -d= -f2 || true)
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
EXTENSION="${REPO}/dist/chrome"
|
|
111
|
+
AGENT_DIR="${REPO}/${RUNTIME_DIR}"
|
|
112
|
+
PROFILE="${CHROME_USER_DATA_DIR:-${AGENT_DIR}/chrome-profile-recipe}"
|
|
113
|
+
if [ ! -f "${PROFILE}/Default/Preferences" ] && [ -f "${AGENT_DIR}/chrome-profile-pw/Default/Preferences" ]; then
|
|
114
|
+
PROFILE="${AGENT_DIR}/chrome-profile-pw"
|
|
115
|
+
fi
|
|
116
|
+
WALLET_FIXTURE="${AGENT_DIR}/wallet-fixture.json"
|
|
117
|
+
# Extension id resolution is delegated to the installed recipe-runner — the
|
|
118
|
+
# single source of truth (deterministic id from the loaded dist's manifest key).
|
|
119
|
+
# Honor the configured recipe harness root.
|
|
120
|
+
HARNESS_ROOT="$(harness_root)"
|
|
121
|
+
RUNNER_BIN="${REPO}/${HARNESS_ROOT}/extension/runner/bin/metamask-recipe"
|
|
122
|
+
|
|
123
|
+
# --- Preflight checks ---
|
|
124
|
+
if [ ! -f "${EXTENSION}/manifest.json" ]; then
|
|
125
|
+
echo "FAIL: No build at ${EXTENSION}/manifest.json"
|
|
126
|
+
echo " Run preflight or: cd ${REPO} && PORT=${WATCHER_PORT:-9011} yarn start"
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
if [ ! -f "${WALLET_FIXTURE}" ]; then
|
|
131
|
+
echo "FAIL: wallet fixture missing at ${WALLET_FIXTURE}"
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [ ! -f "${PROFILE}/Default/Preferences" ]; then
|
|
136
|
+
echo "FAIL: existing browser profile not found at ${PROFILE}"
|
|
137
|
+
echo " Reopen only reuses an already-prepared profile. Run: metamask-recipe runtime-launch --adapter extension --target ${REPO} --cdp-port ${CDP_PORT} --chrome-user-data-dir ${PROFILE} --json"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
WEBPACK_PID=$(cat "${AGENT_DIR}/webpack.pid" 2>/dev/null || true)
|
|
142
|
+
if [ -z "$WEBPACK_PID" ] || ! kill -0 "$WEBPACK_PID" 2>/dev/null; then
|
|
143
|
+
echo "WARN: webpack not running (no live PID in ${AGENT_DIR}/webpack.pid)"
|
|
144
|
+
echo " Hot reload won't work. Start it: cd ${REPO} && PORT=${WATCHER_PORT:-9011} yarn start"
|
|
145
|
+
WEBPACK_PID=""
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
wait_for_browser_exit() {
|
|
149
|
+
local pid="$1"
|
|
150
|
+
if [ -z "$pid" ]; then
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
for _ in $(seq 1 30); do
|
|
154
|
+
if ! kill -0 "$pid" 2>/dev/null; then
|
|
155
|
+
break
|
|
156
|
+
fi
|
|
157
|
+
sleep 1
|
|
158
|
+
done
|
|
159
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
160
|
+
echo "[cleanup] Browser PID ${pid} did not exit after SIGTERM; forcing kill"
|
|
161
|
+
kill -KILL "$pid" 2>/dev/null || true
|
|
162
|
+
fi
|
|
163
|
+
for _ in $(seq 1 10); do
|
|
164
|
+
if [ ! -e "${PROFILE}/SingletonLock" ] && [ ! -e "${PROFILE}/SingletonSocket" ] && [ ! -e "${PROFILE}/SingletonCookie" ]; then
|
|
165
|
+
return 0
|
|
166
|
+
fi
|
|
167
|
+
sleep 1
|
|
168
|
+
done
|
|
169
|
+
rm -f "${PROFILE}/SingletonLock" "${PROFILE}/SingletonSocket" "${PROFILE}/SingletonCookie" 2>/dev/null || true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
cleanup_stale_profile_locks() {
|
|
173
|
+
local lock_path="${PROFILE}/SingletonLock"
|
|
174
|
+
if [ ! -L "$lock_path" ]; then
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
local target pid
|
|
178
|
+
target=$(readlink "$lock_path" 2>/dev/null || true)
|
|
179
|
+
pid=$(printf '%s' "$target" | sed -E 's/.*-([0-9]+)$/\1/' )
|
|
180
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
181
|
+
echo "[cleanup] Profile lock owned by live Chromium PID ${pid}"
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
echo "[cleanup] Removing stale Chromium profile lock (${target:-unknown})"
|
|
185
|
+
rm -f "${PROFILE}/SingletonLock" "${PROFILE}/SingletonSocket" "${PROFILE}/SingletonCookie" 2>/dev/null || true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
kill_profile_processes() {
|
|
189
|
+
local pids
|
|
190
|
+
pids=$(pgrep -f "${PROFILE}" 2>/dev/null || true)
|
|
191
|
+
if [ -z "$pids" ]; then
|
|
192
|
+
return 0
|
|
193
|
+
fi
|
|
194
|
+
echo "[cleanup] Killing lingering profile processes: $(echo "$pids" | tr '\n' ' ')"
|
|
195
|
+
echo "$pids" | xargs kill -TERM 2>/dev/null || true
|
|
196
|
+
sleep 2
|
|
197
|
+
pids=$(pgrep -f "${PROFILE}" 2>/dev/null || true)
|
|
198
|
+
if [ -n "$pids" ]; then
|
|
199
|
+
echo "$pids" | xargs kill -KILL 2>/dev/null || true
|
|
200
|
+
fi
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# --- Kill existing browser (by PID only — don't touch other slots) ---
|
|
204
|
+
PREV_PID=$(cat "${AGENT_DIR}/browser.pid" 2>/dev/null || true)
|
|
205
|
+
if [ -n "$PREV_PID" ] && kill -0 "$PREV_PID" 2>/dev/null; then
|
|
206
|
+
echo "[cleanup] Killing browser PID ${PREV_PID}"
|
|
207
|
+
kill "$PREV_PID" 2>/dev/null || true
|
|
208
|
+
wait_for_browser_exit "$PREV_PID"
|
|
209
|
+
fi
|
|
210
|
+
kill_profile_processes
|
|
211
|
+
cleanup_stale_profile_locks
|
|
212
|
+
rm -f "${AGENT_DIR}/browser.pid" "${AGENT_DIR}/extension.id" "${AGENT_DIR}/launcher.pid"
|
|
213
|
+
echo "[cleanup] Reusing profile ${PROFILE}"
|
|
214
|
+
|
|
215
|
+
# Clear crash recovery state + re-enable any previously-disabled extensions
|
|
216
|
+
# (profile Preferences persists extensions.settings.<id>.state=0 after unsafe
|
|
217
|
+
# shutdowns; --load-extension re-advertises the path but does not flip it back,
|
|
218
|
+
# leaving MV3 service workers inert).
|
|
219
|
+
if [ -f "${PROFILE}/Default/Preferences" ]; then
|
|
220
|
+
python3 -c "
|
|
221
|
+
import json
|
|
222
|
+
pf = '${PROFILE}/Default/Preferences'
|
|
223
|
+
prefs = json.load(open(pf))
|
|
224
|
+
prefs['profile'] = prefs.get('profile', {})
|
|
225
|
+
prefs['profile']['exit_type'] = 'Normal'
|
|
226
|
+
prefs['profile']['exited_cleanly'] = True
|
|
227
|
+
ext_settings = prefs.get('extensions', {}).get('settings', {})
|
|
228
|
+
for ext_id, ext_prefs in ext_settings.items():
|
|
229
|
+
if isinstance(ext_prefs, dict) and ext_prefs.get('state') != 1:
|
|
230
|
+
ext_prefs['state'] = 1
|
|
231
|
+
ext_prefs.pop('disable_reasons', None)
|
|
232
|
+
json.dump(prefs, open(pf, 'w'))
|
|
233
|
+
" 2>/dev/null && echo "[cleanup] Cleared crash recovery state + re-enabled extensions"
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
# Readiness gate: wait until webpack emits a loadable background entry before
|
|
237
|
+
# freezing dist/chrome. The entry differs by manifest version — MV3 emits
|
|
238
|
+
# service-worker.js, MV2 emits scripts/app-init.js — so accept EITHER (gated on
|
|
239
|
+
# manifest.json). Hardcoding the MV2 app-init.js path made this hang the full
|
|
240
|
+
# timeout and FAIL on MV3 builds, which is the #1 reason agents can't launch the
|
|
241
|
+
# browser and then burn tokens debugging. Deterministic and MV-agnostic.
|
|
242
|
+
build_entry_ready() {
|
|
243
|
+
[ -s "${EXTENSION}/manifest.json" ] || return 1
|
|
244
|
+
[ -s "${EXTENSION}/service-worker.js" ] || [ -s "${EXTENSION}/scripts/app-init.js" ]
|
|
245
|
+
}
|
|
246
|
+
for _ in $(seq 1 60); do
|
|
247
|
+
build_entry_ready && break
|
|
248
|
+
sleep 1
|
|
249
|
+
done
|
|
250
|
+
|
|
251
|
+
if ! build_entry_ready; then
|
|
252
|
+
echo "FAIL: no loadable background entry (service-worker.js [MV3] or scripts/app-init.js [MV2]) under ${EXTENSION} after waiting for webpack output"
|
|
253
|
+
exit 1
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
resume_webpack() {
|
|
257
|
+
if [ -n "${WEBPACK_PID:-}" ]; then
|
|
258
|
+
kill -CONT -"$WEBPACK_PID" 2>/dev/null || kill -CONT "$WEBPACK_PID" 2>/dev/null || true
|
|
259
|
+
fi
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Pause webpack process group so dist/chrome is stable during launch
|
|
263
|
+
if [ -n "$WEBPACK_PID" ]; then
|
|
264
|
+
kill -STOP -"$WEBPACK_PID" 2>/dev/null || kill -STOP "$WEBPACK_PID" 2>/dev/null || true
|
|
265
|
+
echo "[webpack] Paused (PID ${WEBPACK_PID})"
|
|
266
|
+
trap resume_webpack EXIT
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
echo "[reopen] ${SLOT_ID} — CDP:${CDP_PORT:-off}"
|
|
270
|
+
|
|
271
|
+
cd "${REPO}"
|
|
272
|
+
|
|
273
|
+
trap - EXIT
|
|
274
|
+
exec node -e "
|
|
275
|
+
const path = require('path');
|
|
276
|
+
const fs = require('fs');
|
|
277
|
+
const http = require('http');
|
|
278
|
+
const { execSync, execFileSync } = require('child_process');
|
|
279
|
+
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
280
|
+
|
|
281
|
+
const SLOT_ID = '${SLOT_ID}';
|
|
282
|
+
const AGENT_DIR = '${AGENT_DIR}';
|
|
283
|
+
const EXTENSION = '${EXTENSION}';
|
|
284
|
+
const REPO = '${REPO}';
|
|
285
|
+
const RUNNER_BIN = '${RUNNER_BIN}';
|
|
286
|
+
const PROFILE = '${PROFILE}';
|
|
287
|
+
const CDP_PORT = '${CDP_PORT}' || null;
|
|
288
|
+
const WEBPACK_PID = '${WEBPACK_PID}' || null;
|
|
289
|
+
const walletFixture = JSON.parse(fs.readFileSync('${WALLET_FIXTURE}', 'utf8'));
|
|
290
|
+
const PASSWORD = walletFixture.password;
|
|
291
|
+
|
|
292
|
+
if (!PASSWORD) {
|
|
293
|
+
throw new Error('wallet-fixture.json is missing password');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const resumeWebpack = () => {
|
|
297
|
+
if (WEBPACK_PID) { try { process.kill(-Number(WEBPACK_PID), 'SIGCONT'); } catch { try { process.kill(Number(WEBPACK_PID), 'SIGCONT'); } catch {} } }
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
(async () => {
|
|
301
|
+
const args = [
|
|
302
|
+
'--user-data-dir=' + PROFILE,
|
|
303
|
+
'--disable-extensions-except=' + EXTENSION,
|
|
304
|
+
'--load-extension=' + EXTENSION,
|
|
305
|
+
'--disable-background-timer-throttling',
|
|
306
|
+
'--disable-backgrounding-occluded-windows',
|
|
307
|
+
'--disable-renderer-backgrounding',
|
|
308
|
+
'--no-first-run',
|
|
309
|
+
'--no-default-browser-check',
|
|
310
|
+
'--window-size=420,800',
|
|
311
|
+
];
|
|
312
|
+
if (CDP_PORT) args.push('--remote-debugging-port=' + CDP_PORT);
|
|
313
|
+
|
|
314
|
+
const chromiumApp = path.dirname(path.dirname(path.dirname(chromium.executablePath())));
|
|
315
|
+
execFileSync('open', ['-n', '-a', chromiumApp, '--args', ...args], { stdio: 'ignore' });
|
|
316
|
+
for (let i = 0; i < 60; i++) {
|
|
317
|
+
try {
|
|
318
|
+
const version = await new Promise((resolve, reject) => {
|
|
319
|
+
const req = http.get('http://127.0.0.1:' + CDP_PORT + '/json/version', (res) => {
|
|
320
|
+
let body = '';
|
|
321
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
322
|
+
res.on('end', () => resolve(body));
|
|
323
|
+
});
|
|
324
|
+
req.on('error', reject);
|
|
325
|
+
});
|
|
326
|
+
if (version) break;
|
|
327
|
+
} catch {}
|
|
328
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
329
|
+
}
|
|
330
|
+
const browser = await chromium.connectOverCDP('http://127.0.0.1:' + CDP_PORT);
|
|
331
|
+
const ctx = browser.contexts()[0];
|
|
332
|
+
try {
|
|
333
|
+
const browserPid = execSync('lsof -ti tcp:' + CDP_PORT + ' -sTCP:LISTEN | head -1', { timeout: 2000 }).toString().trim();
|
|
334
|
+
if (browserPid) {
|
|
335
|
+
fs.writeFileSync(path.join(AGENT_DIR, 'browser.pid'), browserPid);
|
|
336
|
+
fs.writeFileSync(path.join(AGENT_DIR, 'chromium.pid'), browserPid);
|
|
337
|
+
console.log('[reopen] Chromium PID ' + browserPid);
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
|
|
341
|
+
// Detect extension ID via the recipe-runner — the single source of truth.
|
|
342
|
+
// Deterministic id from the loaded dist's manifest key; replaces a fragile
|
|
343
|
+
// serviceWorkers()[0] scan that grabbed Chrome *component* extensions and
|
|
344
|
+
// pointed home.html at the wrong (non-MetaMask) extension.
|
|
345
|
+
let extId = '';
|
|
346
|
+
try {
|
|
347
|
+
extId = execFileSync(RUNNER_BIN, ['resolve-extension', '--adapter', 'extension', '--target', REPO], { encoding: 'utf8' }).trim();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
resumeWebpack();
|
|
350
|
+
console.error('[FAIL] runner resolve-extension failed: ' + (err && err.message ? err.message : String(err)));
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
if (!/^[a-p]{32}$/.test(extId)) { resumeWebpack(); console.error('[FAIL] runner returned invalid extension id: ' + JSON.stringify(extId)); process.exit(1); }
|
|
354
|
+
console.log('[reopen] Extension: ' + extId + ' (via runner resolve-extension)');
|
|
355
|
+
|
|
356
|
+
// Navigate to MetaMask home
|
|
357
|
+
const homeUrl = 'chrome-extension://' + extId + '/home.html';
|
|
358
|
+
const page = ctx.pages().find(p => p.url().includes('chrome-extension://')) || ctx.pages()[0];
|
|
359
|
+
await page.goto(homeUrl, { waitUntil: 'load', timeout: 15000 });
|
|
360
|
+
|
|
361
|
+
// Set window title to slot ID
|
|
362
|
+
await page.evaluate((id) => { document.title = id + ' \u2014 ' + document.title; }, SLOT_ID);
|
|
363
|
+
|
|
364
|
+
// Wait for runtime state to settle, then unlock if needed.
|
|
365
|
+
const unlockSelector = '[data-testid=\"unlock-password\"]';
|
|
366
|
+
const homeSelector = '[data-testid=\"account-menu-icon\"]';
|
|
367
|
+
let detectedState = 'unknown';
|
|
368
|
+
for (let i = 0; i < 15; i++) {
|
|
369
|
+
if (await page.locator(homeSelector).count()) {
|
|
370
|
+
detectedState = 'unlocked';
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
if (await page.locator(unlockSelector).count()) {
|
|
374
|
+
detectedState = 'locked';
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
if (page.url().includes('/onboarding')) {
|
|
378
|
+
detectedState = 'onboarding';
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (detectedState === 'locked') {
|
|
385
|
+
console.log('[reopen] Unlocking wallet...');
|
|
386
|
+
await page.fill(unlockSelector, PASSWORD);
|
|
387
|
+
await page.click('[data-testid=\"unlock-submit\"]');
|
|
388
|
+
await page.waitForSelector(homeSelector, { timeout: 30000 });
|
|
389
|
+
console.log('[reopen] Unlocked');
|
|
390
|
+
} else if (detectedState === 'onboarding') {
|
|
391
|
+
console.log('[reopen] On onboarding — state injection may have failed');
|
|
392
|
+
} else {
|
|
393
|
+
console.log('[reopen] Screen: ' + page.url());
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Close extra extension tabs
|
|
397
|
+
for (const p of ctx.pages()) {
|
|
398
|
+
if (p !== page && p.url().includes('chrome-extension://')) await p.close().catch(() => {});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
402
|
+
console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
|
|
403
|
+
|
|
404
|
+
// Resume webpack now that browser loaded the extension
|
|
405
|
+
resumeWebpack();
|
|
406
|
+
if (WEBPACK_PID) console.log('[webpack] Resumed');
|
|
407
|
+
process.exit(0);
|
|
408
|
+
})().catch(e => {
|
|
409
|
+
resumeWebpack();
|
|
410
|
+
try { fs.unlinkSync(path.join(AGENT_DIR, 'browser.pid')); } catch {}
|
|
411
|
+
try { fs.unlinkSync(path.join(AGENT_DIR, 'chromium.pid')); } catch {}
|
|
412
|
+
try { fs.unlinkSync(path.join(AGENT_DIR, 'extension.id')); } catch {}
|
|
413
|
+
console.error(e);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
});
|
|
416
|
+
"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// ensure-ready.ts — ensure the extension runtime is attachable.
|
|
2
|
+
// runner/extension/extension-ensure-ready.ts) App/instance control.
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { resolveExtensionId } from './extension-id.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* "Drive the extension to a ready state" — the single source of truth for the
|
|
9
|
+
* open-home + collapse-to-one-tab + verify-healthy logic that host launchers
|
|
10
|
+
* and reopen helpers previously hand-rolled. They left duplicate home tabs, which trips
|
|
11
|
+
* runtime-health's "exactly one home page" contract and makes agents see a
|
|
12
|
+
* broken-looking runtime.
|
|
13
|
+
*
|
|
14
|
+
* Guarantees, given a live CDP port:
|
|
15
|
+
* - the extension id is resolved deterministically (resolveExtensionId),
|
|
16
|
+
* - exactly ONE chrome-extension home.html page target is open (opened if none,
|
|
17
|
+
* extras closed if several),
|
|
18
|
+
* - the runtime is verified healthy (checkExtensionRuntimeHealth).
|
|
19
|
+
*
|
|
20
|
+
* Pure-ish: it only opens/closes tabs to converge on the single-tab invariant;
|
|
21
|
+
* it does not rebuild or relaunch. Unlocking the wallet remains with the host
|
|
22
|
+
* launcher for now (a future increment can fold ensure_unlocked in here too).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface EnsureReadyResult {
|
|
26
|
+
schemaVersion: 1;
|
|
27
|
+
adapter: 'extension';
|
|
28
|
+
target: string;
|
|
29
|
+
cdpPort: number;
|
|
30
|
+
extensionId: string | null;
|
|
31
|
+
opened: boolean;
|
|
32
|
+
homeTabs: { before: number; closed: number; after: number };
|
|
33
|
+
ready: boolean;
|
|
34
|
+
reasonCode: string;
|
|
35
|
+
health: { status: 'PASS' | 'FAIL' | 'unknown'; findings: string[] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CdpTarget {
|
|
39
|
+
id?: string;
|
|
40
|
+
type?: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
|
|
46
|
+
async function jsonList(port: number): Promise<CdpTarget[]> {
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/list`, { signal: AbortSignal.timeout(5000) });
|
|
49
|
+
if (!res.ok) return [];
|
|
50
|
+
const value = await res.json();
|
|
51
|
+
return Array.isArray(value) ? (value as CdpTarget[]) : [];
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function closeTab(port: number, id: string): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
await fetch(`http://127.0.0.1:${port}/json/close/${id}`, { signal: AbortSignal.timeout(4000) });
|
|
60
|
+
} catch {
|
|
61
|
+
// best-effort; a tab that won't close is surfaced by the final health check
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function activateTab(port: number, id: string): Promise<void> {
|
|
66
|
+
try {
|
|
67
|
+
await fetch(`http://127.0.0.1:${port}/json/activate/${id}`, { signal: AbortSignal.timeout(4000) });
|
|
68
|
+
} catch {
|
|
69
|
+
// best-effort; foregrounding only steers the headed-window screen capture onto
|
|
70
|
+
// the wallet — the authoritative readiness signal is the health check below
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Blank / New-Tab pages Chrome opens by default. The headed-window screen capture
|
|
75
|
+
// (device-grid stream) follows whatever tab is foreground, so a lingering New Tab
|
|
76
|
+
// makes the stream show chrome://newtab instead of the wallet. Safe to close once
|
|
77
|
+
// a home tab exists.
|
|
78
|
+
function isStrayTab(url?: string): boolean {
|
|
79
|
+
if (typeof url !== 'string') return false;
|
|
80
|
+
return /^chrome:\/\/newtab\/?$/.test(url) || /^chrome:\/\/new-tab-page\b/.test(url) || url === 'about:blank';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function openHome(port: number, extensionId: string): Promise<boolean> {
|
|
84
|
+
const endpoint = `http://127.0.0.1:${port}/json/new?${encodeURIComponent(`chrome-extension://${extensionId}/home.html`)}`;
|
|
85
|
+
try {
|
|
86
|
+
let res = await fetch(endpoint, { method: 'PUT', signal: AbortSignal.timeout(8000) });
|
|
87
|
+
if (res.status === 404 || res.status === 405) res = await fetch(endpoint, { signal: AbortSignal.timeout(8000) });
|
|
88
|
+
return res.ok;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function homePages(targets: CdpTarget[], extensionId: string): CdpTarget[] {
|
|
95
|
+
const prefix = `chrome-extension://${extensionId}`;
|
|
96
|
+
return targets.filter(
|
|
97
|
+
(t) => t.type === 'page' && typeof t.url === 'string' && t.url.startsWith(prefix) && t.url.includes('/home.html') && t.id,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function ensureExtensionReady(
|
|
102
|
+
target: string,
|
|
103
|
+
options: { cdpPort: number },
|
|
104
|
+
): Promise<EnsureReadyResult> {
|
|
105
|
+
const resolved = path.resolve(target);
|
|
106
|
+
const { cdpPort } = options;
|
|
107
|
+
const base = (extra: Partial<EnsureReadyResult>): EnsureReadyResult => ({
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
adapter: 'extension',
|
|
110
|
+
target: resolved,
|
|
111
|
+
cdpPort,
|
|
112
|
+
extensionId: null,
|
|
113
|
+
opened: false,
|
|
114
|
+
homeTabs: { before: 0, closed: 0, after: 0 },
|
|
115
|
+
ready: false,
|
|
116
|
+
reasonCode: 'unknown',
|
|
117
|
+
health: { status: 'unknown', findings: [] },
|
|
118
|
+
...extra,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { extensionId } = await resolveExtensionId(resolved, { cdpPort });
|
|
122
|
+
if (!extensionId) return base({ reasonCode: 'no-extension-id' });
|
|
123
|
+
|
|
124
|
+
const homes = homePages(await jsonList(cdpPort), extensionId);
|
|
125
|
+
const before = homes.length;
|
|
126
|
+
|
|
127
|
+
// Converge on exactly one home tab.
|
|
128
|
+
// - 0 tabs → open one.
|
|
129
|
+
// - 1 tab → keep it (cheap; the health check below validates it).
|
|
130
|
+
// - >1 tabs → close ALL and open one fresh. /json/list alone can't tell a
|
|
131
|
+
// healthy tab from a chrome-error one (a crashed page still lists its
|
|
132
|
+
// intended url), so keeping the first could discard the good tab and keep a
|
|
133
|
+
// broken one. A fresh tab is deterministically clean.
|
|
134
|
+
let opened = false;
|
|
135
|
+
let closed = 0;
|
|
136
|
+
if (homes.length > 1) {
|
|
137
|
+
for (const h of homes) {
|
|
138
|
+
await closeTab(cdpPort, String(h.id));
|
|
139
|
+
closed += 1;
|
|
140
|
+
}
|
|
141
|
+
await sleep(500);
|
|
142
|
+
opened = await openHome(cdpPort, extensionId);
|
|
143
|
+
await sleep(1500);
|
|
144
|
+
} else if (homes.length === 0) {
|
|
145
|
+
opened = await openHome(cdpPort, extensionId);
|
|
146
|
+
await sleep(1500);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Drop stray New Tab / blank pages and foreground the wallet so the headed
|
|
150
|
+
// window captured by the device-grid stream shows MetaMask, not chrome://newtab.
|
|
151
|
+
// Only prune once a home tab exists, so the window is never left with zero tabs
|
|
152
|
+
// (which would close it).
|
|
153
|
+
const converged = await jsonList(cdpPort);
|
|
154
|
+
const homesNow = homePages(converged, extensionId);
|
|
155
|
+
if (homesNow.length >= 1) {
|
|
156
|
+
for (const t of converged) {
|
|
157
|
+
if (t.type === 'page' && t.id && isStrayTab(t.url)) await closeTab(cdpPort, String(t.id));
|
|
158
|
+
}
|
|
159
|
+
if (homesNow[0]?.id) await activateTab(cdpPort, String(homesNow[0].id));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const after = homePages(await jsonList(cdpPort), extensionId).length;
|
|
163
|
+
|
|
164
|
+
// Verify with the single health source. Lazy import: extension-runtime.ts
|
|
165
|
+
// loads the recipe harness at module scope, so it must not be pulled into
|
|
166
|
+
// any no-CDP path.
|
|
167
|
+
let health: EnsureReadyResult['health'] = { status: 'unknown', findings: [] };
|
|
168
|
+
try {
|
|
169
|
+
const { checkExtensionRuntimeHealth } = await import('./runtime.ts');
|
|
170
|
+
const report = await checkExtensionRuntimeHealth(resolved, cdpPort);
|
|
171
|
+
health = { status: report.status, findings: report.findings };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
health = { status: 'FAIL', findings: [error instanceof Error ? error.message : String(error)] };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ready = health.status === 'PASS' && after === 1;
|
|
177
|
+
return base({
|
|
178
|
+
extensionId,
|
|
179
|
+
opened,
|
|
180
|
+
homeTabs: { before, closed, after },
|
|
181
|
+
ready,
|
|
182
|
+
reasonCode: ready ? 'ready' : after !== 1 ? 'tab-count' : 'unhealthy',
|
|
183
|
+
health,
|
|
184
|
+
});
|
|
185
|
+
}
|