@deeeed/metamask-harness 0.13.0 → 0.14.1
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 +43 -4
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +15 -3
- package/adapters/extension/lib/extension-id.cjs +36 -0
- package/adapters/extension/live.sh +5 -3
- package/adapters/extension/readiness.mjs +48 -0
- package/adapters/extension/reattach.sh +163 -55
- package/adapters/extension/sidepanel-toggle.sh +96 -24
- package/adapters/extension/verify.sh +15 -4
- package/adapters/extension/wallet-fixture-state.cjs +6 -11
- package/adapters/mobile/open-device.sh +32 -5
- package/adapters/mobile/start-metro.sh +15 -1
- package/adapters/mobile/verify.sh +50 -13
- package/adapters/shared/log-tui.mjs +7 -3
- package/adapters/shared/resolve-slot-ports-core.mjs +14 -4
- package/dist/adapters/core/surface.js +1 -0
- package/dist/adapters/extension/runtime.js +14 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/adapters/mobile/provision.js +49 -4
- package/dist/adapters/mobile/surface.js +1 -0
- package/dist/adapters/slot-ports.js +11 -4
- package/dist/cli-commands.js +5 -4
- package/dist/cli.js +4 -0
- package/dist/commands/call.js +79 -6
- package/dist/commands/check.js +326 -0
- package/dist/commands/core-readiness.js +75 -0
- package/dist/commands/device-target.js +123 -13
- package/dist/commands/doctor.js +30 -18
- package/dist/commands/fixtures.js +2 -1
- package/dist/commands/launch/extension.js +105 -9
- package/dist/commands/launch/index.js +116 -15
- package/dist/commands/mobile-device-view.js +140 -0
- package/dist/commands/parse-args.js +4 -1
- package/dist/commands/recipe-quality.js +6 -2
- package/dist/commands/run-engine.js +90 -5
- package/dist/commands/run-report.js +115 -0
- package/dist/commands/run.js +115 -8
- package/dist/commands/self-test.js +1 -1
- package/dist/commands/shared.js +2 -1
- package/dist/commands/status-probe.js +9 -3
- package/dist/commands/status.js +45 -50
- package/dist/live-adapter-contract.js +5 -1
- package/dist/mm-harness-cli.js +68 -27
- package/dist/recipe-files.js +14 -0
- package/docs/CLI-SPEC.md +25 -0
- package/docs/architecture.md +2 -2
- package/docs/live-adapter-contract.md +1 -1
- package/docs/recipe-libraries.md +21 -18
- package/library/actions/extension/platform/cdp.mjs +7 -4
- package/library/recipes/{app-lifecycle-android-smoke.mobile.recipe.json → app/lifecycle.android-smoke.mobile.recipe.json} +1 -1
- package/library/recipes/{perps-performance.mobile.recipe.json → perps/performance.mobile.recipe.json} +1 -1
- package/library/recipes/perps/smoke.core.recipe.json +39 -0
- package/library/recipes/perps/smoke.extension.recipe.json +51 -0
- package/library/recipes/perps/smoke.mobile.recipe.json +51 -0
- package/library/recipes/{action-validation.extension.recipe.json → runner/action-validation.extension.recipe.json} +7 -7
- package/library/recipes/{action-validation.mobile.recipe.json → runner/action-validation.mobile.recipe.json} +7 -7
- package/package.json +1 -1
- /package/library/recipes/{perps-lifecycle.recipe.json → perps/lifecycle.recipe.json} +0 -0
- /package/library/recipes/{order-lifecycle.core.recipe.json → perps/order-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-background-resume.mobile.recipe.json → perps/performance.background-resume.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-cold-start.mobile.recipe.json → perps/performance.cold-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{perps-performance-warm-start.mobile.recipe.json → perps/performance.warm-start.mobile.recipe.json} +0 -0
- /package/library/recipes/{read-markets.core.recipe.json → perps/read-markets.core.recipe.json} +0 -0
- /package/library/recipes/{trading-lifecycle.core.recipe.json → perps/trading-lifecycle.core.recipe.json} +0 -0
- /package/library/recipes/{smoke.extension.recipe.json → runner/smoke.extension.recipe.json} +0 -0
- /package/library/recipes/{smoke.mobile.recipe.json → runner/smoke.mobile.recipe.json} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.14.1 - 2026-07-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `mm-harness check diff` runs bounded repo-local validation for the active git diff, writing `validation-summary.json`/`.md` plus per-check logs. The fast profile runs changed-file ESLint, Prettier, and changed test files; `--profile full` also runs the repo typecheck when available. The default base follows the active PR base or repo remote HEAD before falling back to common branch names.
|
|
9
|
+
- `mm-harness run` writes a concise human report (`report.md`) and prints a colored step summary for every adapter, so successful Core/Mobile/Extension recipe runs are scannable without opening raw trace files.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Mobile slot targeting now refuses a platform launch/run/call when `--platform ios|android` conflicts with the configured default target, preventing an iOS-pinned slot from silently stealing a shared Android device; `status` and `doctor` scope device lists to the configured target by default and `status` reports other reachable bridges as additional targets that can be driven with `--device`.
|
|
13
|
+
- Mobile `status` bases `Next:` on the configured default target only, avoids assigning an additional Android bridge to a same-platform selected device, and reports stale Android reverse ports when the selected device is not connected to the slot Metro.
|
|
14
|
+
- Mobile `doctor` now reports the same additional reachable targets as `status`, so diagnostics and the home view agree on which devices can be driven with `--device`.
|
|
15
|
+
- Mobile `status` and `doctor` reuse one bridge probe for scoped and additional reachable devices, preserving the bounded probe window when the bridge is down.
|
|
16
|
+
- Mobile slot env resolution now preserves a configured Android Metro device name when an ADB serial is also present, instead of overwriting it with the serial.
|
|
17
|
+
- `mm-harness run <action-name>` now teaches `mm-harness call <action-name>` when the argument matches an action instead of a recipe, before requiring `--artifacts-dir`.
|
|
18
|
+
- Core Perps recipes now fail fast with a targeted `yarn install --immutable` teaching step when the target checkout is missing controller dependencies, including Perps actions reached through called library flows.
|
|
19
|
+
- Extension quick reattach refreshes the loaded runtime-dist in place and preserves an explicitly requested dapp/start URL instead of closing it during fullscreen cleanup.
|
|
20
|
+
- `mm-harness check diff` ignores its own artifact directory when that directory lives inside the target checkout, so repeated runs do not validate generated check artifacts.
|
|
21
|
+
- Mobile launch treats missing slot simulators or missing dev clients as provisioning failures, not Metro-healable failures, and prints the matching `mm-harness provision runway ios` recovery command without retrying the wrong layer.
|
|
22
|
+
- Mobile Runway provisioning recreates a deleted slot simulator by resolving the latest available iOS runtime and iPhone simulator type from `simctl`, so accidental simulator deletion is one-command repairable.
|
|
23
|
+
- Extension readiness validates local-only CDP websocket targets and sanitizes slot title data before stamping browser tab titles.
|
|
24
|
+
|
|
25
|
+
## 0.14.0 - 2026-07-08
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `mm-harness call` accepts action fields as direct `key=value` shorthand in addition to repeated `--arg k=v`, so commands like `mm-harness call ui.navigate page=perps` now match the examples agents naturally write.
|
|
29
|
+
- Packaged recipes are grouped by domain under `library/recipes/<domain>/` and still resolve by dotted names (`perps.smoke`, `runner.smoke`) with legacy aliases for the former flat names.
|
|
30
|
+
- Packaged `perps.smoke` recipes for mobile, extension, and core provide an explicit non-mutating Perps domain smoke. The existing bare `smoke` alias remains a runner/transport smoke for compatibility.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- The top-level help SLOT banner now labels the slot dev-server port by adapter, so extension checkouts show `webpack :<port>` instead of the mobile-only `metro :<port>`.
|
|
34
|
+
- Hidden internal runtime commands remain executable through the installed `mm-harness` bin, fixing Extension verify warnings where `runtime-decision` returned `{}` because the wrapper blocked its own shipped helper.
|
|
35
|
+
- Extension launch/readiness now stamps the live home tab title with the Farmslot slot id after MetaMask has settled, so headed browser windows identify their owning slot.
|
|
36
|
+
- Extension quick launch now reattaches to a live slot browser when CDP belongs to this checkout's Chrome profile and dist is fresh, even if the webpack watcher is not currently listening; foreign CDP profiles still force the clean path.
|
|
37
|
+
- `status` now hydrates slot runtime context before device enumeration, so a Farmslot checkout pinned to `IOS_SIMULATOR=mm-1` reports only the selected slot simulator instead of every booted simulator/device on the machine. Live status also updates `Next:` to `mm-harness logs` when the selected bridge is reachable.
|
|
38
|
+
- The tmux Metro window opened by `launch ios|android` now streams the Metro output it writes to `metro.log` instead of opening a blank server window.
|
|
39
|
+
- `log-tui watch` now matches success/failure patterns only against log content emitted after the watcher starts, so stale markers already present in long-lived Metro/webpack logs cannot false-pass readiness checks.
|
|
40
|
+
- `start-metro` now reaps any existing mobile console-forwarder attached to the same Metro port before spawning this slot's forwarder, even when the old process was launched from another checkout or package install path.
|
|
41
|
+
|
|
3
42
|
## 0.13.0 - 2026-07-08
|
|
4
43
|
|
|
5
44
|
### Added
|
|
@@ -29,7 +68,7 @@
|
|
|
29
68
|
### Added
|
|
30
69
|
- Mobile recipes can now use the standard outer `app.lifecycle` action for deterministic performance start states without rebuilding (Android background/foreground/terminate/restart, iOS simulator launch/terminate/restart through the shared Farmslot lifecycle adapter).
|
|
31
70
|
- `fixtures set --device <serial|udid|name>` now pins mobile fixture sync to the requested device using the same targeting rules as `run`, `call`, and `doctor`.
|
|
32
|
-
- New lifecycle-controlled measured flows in the packaged library: `app
|
|
71
|
+
- New lifecycle-controlled measured flows in the packaged library: `app.lifecycle.android-smoke`, `perps.performance.warm-start`, `perps.performance.background-resume`, `perps.performance.cold-start`; performance recipes now keep lifecycle/unlock in `setup[]`/`startState` so measured nodes carry only CUF timings.
|
|
33
72
|
|
|
34
73
|
### Changed
|
|
35
74
|
- `@farmslot/recipe-harness` dependency raised to `^0.4.0` (ships the `app.lifecycle` adapter — the `FARMSLOT_ROOT` local-source fallback is no longer needed at runtime).
|
|
@@ -46,9 +85,9 @@
|
|
|
46
85
|
## 0.10.0 - 2026-07-07
|
|
47
86
|
|
|
48
87
|
### Added
|
|
49
|
-
- **Canonical `perps
|
|
88
|
+
- **Canonical `perps.performance` measured flow** (`library/recipes/perps/performance.mobile.recipe.json`): unlock → open the Perps market list → read live state → open a market detail — one node per user-visible step with stable node names, so the per-node `duration`s in `trace.json` are the timings you monitor. Run pinned: `mm-harness run perps.performance --device <serial> --heal off`. Device-proven end-to-end on a physical Pixel.
|
|
50
89
|
- **`run <name>` resolves personal/team recipe libraries.** Previously only the packaged library was probed by name; custom recipes ran by path. Sources resolve in library-precedence order (personal > team shadow the packaged canonical — a same-named personal recipe wins), path-shaped args never probe libraries, and a miss teaches which sources were searched. Zero-flag default: `$FARMSLOT_HOME/recipe-library`.
|
|
51
|
-
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps
|
|
90
|
+
- **Custom-library walkthrough** ("Your own measured flow" in `docs/recipe-libraries.md`): a peer engineer scaffolds a personal library, copies the canonical flow, retargets the nodes to their journey, and runs it by name. Every step is executed by the `perps.performance-recipe` contract test, so the doc cannot drift from reality.
|
|
52
91
|
- **`call` declared on mobile and extension manifests** (was core-only) with self-discovery metadata — personal `flows/` segments are now usable from mobile/extension recipes. Honest limitation documented: flows carry no adapter dimension in the protocol yet, so a cross-adapter `call` fails at live-run rather than plan time.
|
|
53
92
|
|
|
54
93
|
### Fixed
|
|
@@ -71,7 +110,7 @@
|
|
|
71
110
|
## 0.8.0 - 2026-07-07
|
|
72
111
|
|
|
73
112
|
### Added
|
|
74
|
-
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `smoke.mobile.recipe.json`), then adapterless (`run perps
|
|
113
|
+
- **`run <name>` — the recipe positional resolves packaged-library recipe names.** An existing file path (absolute or cwd-relative) wins; otherwise the arg is a library recipe name, adapter-scoped first (`run smoke` on mobile → `library/recipes/runner/smoke.mobile.recipe.json`), then adapterless (`run perps.lifecycle` → `library/recipes/perps/lifecycle.recipe.json`), then the exact library filename. A miss teaches `RECIPE_NOT_FOUND` naming the library recipes available for the adapter — no more `$LIB`-style path prefixes in docs or demo scripts. A directory shadowing a library name never wins, and path-shaped args (containing a separator) never fall through to the library.
|
|
75
114
|
- **`--device <udid|serial|name>` — first-class mobile device targeting on `run`, `call`, and `doctor`.** Resolves against connected devices (adb + booted simulators; exact id first, then exact name with an ambiguity teaching error) and sets the same env `launch --device` does. Mobile `run`/`call` **without** `--device` while more than one targetable device is connected (across android + ios; android state `device`, iOS `Booted`) fail fast listing the devices and the `--device` hint, so a recipe never lands on the wrong device silently. `run --plan`/`--list` are exempt; extension/core teach. Errors honor `--json` with structured envelopes.
|
|
76
115
|
- **`status` gains `devices[]`** (additive): `{platform, id, name, state, selected}` for connected android devices and booted iOS simulators on mobile checkouts — the `yarn a:status` successor; `selected` reflects the current env pinning.
|
|
77
116
|
|
|
@@ -127,6 +127,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(h
|
|
|
127
127
|
copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
|
|
128
128
|
copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
|
|
129
129
|
copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
130
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
|
|
130
131
|
makeExecutableTree(path.join(harnessDir, 'scripts'));
|
|
131
132
|
fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
|
|
132
133
|
|
|
@@ -23,6 +23,7 @@ const fs = require('node:fs');
|
|
|
23
23
|
const os = require('node:os');
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { execFileSync, spawn, spawnSync } = require('node:child_process');
|
|
26
|
+
const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
|
|
26
27
|
|
|
27
28
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
28
29
|
console.log(
|
|
@@ -74,6 +75,10 @@ const ownedPids = new Set(listenerPids);
|
|
|
74
75
|
const previousPid = readPidFile(args['chrome-pid']);
|
|
75
76
|
if (previousPid !== null && processLoadsProfile(previousPid, args.profile)) ownedPids.add(previousPid);
|
|
76
77
|
terminatePids([...ownedPids]);
|
|
78
|
+
if (args['reset-profile'] !== undefined) {
|
|
79
|
+
fs.rmSync(args.profile, { recursive: true, force: true });
|
|
80
|
+
fs.mkdirSync(args.profile, { recursive: true });
|
|
81
|
+
}
|
|
77
82
|
removeProfileSingletonLocks(args.profile);
|
|
78
83
|
|
|
79
84
|
// --stop-only: release the profile (owned Chrome terminated, singleton locks
|
|
@@ -87,6 +92,7 @@ if (args['stop-only'] !== undefined) {
|
|
|
87
92
|
const logFd = fs.openSync(args['chrome-log'], 'a');
|
|
88
93
|
let child;
|
|
89
94
|
try {
|
|
95
|
+
const initialUrl = args['start-url'] || extensionHomeUrl(args['extension-dir']) || 'chrome://extensions/';
|
|
90
96
|
child = spawn(args['chrome-bin'], [
|
|
91
97
|
`--user-data-dir=${args.profile}`,
|
|
92
98
|
'--remote-debugging-address=127.0.0.1',
|
|
@@ -100,9 +106,10 @@ try {
|
|
|
100
106
|
'--disable-features=ExtensionContentVerification,DisableLoadExtensionCommandLineSwitch',
|
|
101
107
|
`--disable-extensions-except=${args['extension-dir']}`,
|
|
102
108
|
`--load-extension=${args['extension-dir']}`,
|
|
103
|
-
// Initial tab:
|
|
104
|
-
// extension
|
|
105
|
-
|
|
109
|
+
// Initial tab: caller's dapp URL when provided, else the extension home. The
|
|
110
|
+
// extension home avoids leaving disposable chrome://newtab/ or extensions
|
|
111
|
+
// manager tabs in the operator's slot browser.
|
|
112
|
+
initialUrl,
|
|
106
113
|
], {
|
|
107
114
|
detached: true,
|
|
108
115
|
env: {
|
|
@@ -228,3 +235,8 @@ function removeProfileSingletonLocks(profile) {
|
|
|
228
235
|
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
229
236
|
}
|
|
230
237
|
}
|
|
238
|
+
|
|
239
|
+
function extensionHomeUrl(extensionDir) {
|
|
240
|
+
const id = extensionIdFromExtensionDir(extensionDir);
|
|
241
|
+
return id ? `chrome-extension://${id}/home.html` : '';
|
|
242
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
// Chromium extension IDs encode each SHA-256 nibble as a-p.
|
|
8
|
+
const CHROMIUM_EXTENSION_ID_ALPHABET = 'abcdefghijklmnop';
|
|
9
|
+
|
|
10
|
+
function extensionIdFromManifestKey(keyBase64) {
|
|
11
|
+
if (typeof keyBase64 !== 'string' || !keyBase64) return '';
|
|
12
|
+
const digest = crypto.createHash('sha256').update(Buffer.from(keyBase64, 'base64')).digest();
|
|
13
|
+
return [...digest.subarray(0, 16)]
|
|
14
|
+
.map((byte) => `${CHROMIUM_EXTENSION_ID_ALPHABET[byte >> 4]}${CHROMIUM_EXTENSION_ID_ALPHABET[byte & 0x0f]}`)
|
|
15
|
+
.join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extensionIdFromManifestFile(manifestPath) {
|
|
19
|
+
try {
|
|
20
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
21
|
+
return extensionIdFromManifestKey(manifest.key);
|
|
22
|
+
} catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extensionIdFromExtensionDir(extensionDir) {
|
|
28
|
+
return extensionIdFromManifestFile(path.join(extensionDir, 'manifest.json'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
CHROMIUM_EXTENSION_ID_ALPHABET,
|
|
33
|
+
extensionIdFromManifestKey,
|
|
34
|
+
extensionIdFromManifestFile,
|
|
35
|
+
extensionIdFromExtensionDir,
|
|
36
|
+
};
|
|
@@ -90,6 +90,8 @@ if [ -z "$VERIFY_SH" ]; then
|
|
|
90
90
|
fi
|
|
91
91
|
TARGET="$(cd "$TARGET" && pwd)"
|
|
92
92
|
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
93
|
+
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
94
|
+
CHROME_PROFILE_NAME="${RECIPE_CHROME_PROFILE_NAME:-chrome-profile}"
|
|
93
95
|
# Runner bin (installed wrapper → source runner). Used by the watch prepare path
|
|
94
96
|
# to defer the cache-clear DECISION to `runtime-decision` (single source) and to
|
|
95
97
|
# record the deps/cache baseline after a confirmed-good build.
|
|
@@ -118,12 +120,12 @@ if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
|
|
|
118
120
|
# Stable beside the profile: the reused profile registers the unpacked
|
|
119
121
|
# extension by PATH; a per-run snapshot path gets pruned and the next launch
|
|
120
122
|
# loads nothing ("No EIP-6963 Provider Detected").
|
|
121
|
-
RUNTIME_DIST_ABS="$TARGET
|
|
123
|
+
RUNTIME_DIST_ABS="$TARGET/$RUNTIME_DIR/$RUNTIME_DIST_DIR"
|
|
122
124
|
# Stable per-checkout profile: successive runs recognize their own Chrome
|
|
123
125
|
# (ownership guard matches on --user-data-dir), take it over cleanly, and the
|
|
124
126
|
# seeded wallet persists across relaunches. A per-run profile made every
|
|
125
127
|
# relaunch "foreign" and let live/ pruning delete profiles still in use.
|
|
126
|
-
PROFILE_ABS="${CHROME_USER_DATA_DIR:-$TARGET
|
|
128
|
+
PROFILE_ABS="${CHROME_USER_DATA_DIR:-$TARGET/$RUNTIME_DIR/$CHROME_PROFILE_NAME}"
|
|
127
129
|
FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
|
|
128
130
|
FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
|
|
129
131
|
# Wallet fixture resolution chain + provenance live in seed-fixture.sh.
|
|
@@ -241,7 +243,7 @@ NODE
|
|
|
241
243
|
if $START_WATCH; then
|
|
242
244
|
prepare_parts+=("bash ${quoted_start_watch} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
|
|
243
245
|
fi
|
|
244
|
-
prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
|
|
246
|
+
prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1 --reset-profile 1 --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}")
|
|
245
247
|
prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
|
|
246
248
|
# Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
|
|
247
249
|
# manifest._flags wins over the fetched ClientConfigApi value. No-op unless
|
|
@@ -100,6 +100,28 @@ function readExpectedExtensionId(target) {
|
|
|
100
100
|
return /^[a-p]{32}$/.test(id) ? id : '';
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
function readSlotId(target) {
|
|
104
|
+
for (const key of ['RECIPE_SLOT_ID', 'SLOT_ID', 'FARMSLOT_SLOT_ID']) {
|
|
105
|
+
const value = sanitizeSlotId(process.env[key]);
|
|
106
|
+
if (value) return value;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, recipeRuntimeDir(), 'agentic-runtime.json'), 'utf8'));
|
|
110
|
+
const value = sanitizeSlotId(runtimeContext.slotId);
|
|
111
|
+
if (value) {
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// No slot context: standalone harness users do not need a custom browser title.
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function sanitizeSlotId(value) {
|
|
121
|
+
const slotId = typeof value === 'string' ? value.trim() : '';
|
|
122
|
+
return /^[A-Za-z0-9._:-]{1,64}$/u.test(slotId) ? slotId : '';
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
function writeExtensionId(target, extensionId) {
|
|
104
126
|
if (!/^[a-p]{32}$/.test(extensionId)) return false;
|
|
105
127
|
const idPath = extensionIdPath(target);
|
|
@@ -118,6 +140,12 @@ function assertLocalUrl(url) {
|
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
function assertLocalWebSocketUrl(url) {
|
|
144
|
+
if (!/^ws:\/\/(?:127\.0\.0\.1|localhost):\d+\//u.test(String(url))) {
|
|
145
|
+
throw new Error(`readiness: refusing non-local websocket URL: ${url}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
121
149
|
function httpJson(url, timeoutMs = 3000) {
|
|
122
150
|
assertLocalUrl(url);
|
|
123
151
|
return new Promise((resolve, reject) => {
|
|
@@ -176,6 +204,7 @@ function resolveWebSocket(target) {
|
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
|
|
207
|
+
assertLocalWebSocketUrl(webSocketDebuggerUrl);
|
|
179
208
|
const WebSocketImpl = resolveWebSocket(target);
|
|
180
209
|
if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable in this Node runtime' };
|
|
181
210
|
return new Promise((resolve, reject) => {
|
|
@@ -333,6 +362,7 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
333
362
|
// Inspect only when the chosen page exposes a websocket. A present-but-attached
|
|
334
363
|
// tab has none; skip inspection and report it rather than opening a duplicate.
|
|
335
364
|
if (pageTarget && typeof pageTarget.webSocketDebuggerUrl === 'string') {
|
|
365
|
+
const slotId = readSlotId(target);
|
|
336
366
|
ui = await cdpEvaluate(
|
|
337
367
|
target,
|
|
338
368
|
pageTarget.webSocketDebuggerUrl,
|
|
@@ -373,6 +403,24 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
373
403
|
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
374
404
|
});
|
|
375
405
|
}
|
|
406
|
+
if (pageInspected && slotId) {
|
|
407
|
+
const stampedTitle = await cdpEvaluate(
|
|
408
|
+
target,
|
|
409
|
+
pageTarget.webSocketDebuggerUrl,
|
|
410
|
+
`(() => {
|
|
411
|
+
const slot = ${JSON.stringify(slotId)};
|
|
412
|
+
const current = document.title || 'MetaMask';
|
|
413
|
+
const base = current.replace(/^.+?\\s+—\\s+/, '') || 'MetaMask';
|
|
414
|
+
const next = slot + ' — ' + base;
|
|
415
|
+
if (document.title !== next) document.title = next;
|
|
416
|
+
return document.title;
|
|
417
|
+
})()`,
|
|
418
|
+
);
|
|
419
|
+
if (stampedTitle && !stampedTitle.skipped) {
|
|
420
|
+
ui.title = stampedTitle;
|
|
421
|
+
ui.slotTitleApplied = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
376
424
|
}
|
|
377
425
|
return {
|
|
378
426
|
browser: version.Browser || 'unknown',
|
|
@@ -33,6 +33,7 @@ START_URL="${EXTENSION_START_URL:-}"
|
|
|
33
33
|
EXT_ID="${EXT_ID:-}"
|
|
34
34
|
DIST_DIR="dist/chrome"
|
|
35
35
|
SETTLE_MS="8000"
|
|
36
|
+
DISPLAY_MODE="${EXTENSION_DISPLAY_MODE:-fullscreen}"
|
|
36
37
|
|
|
37
38
|
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
|
|
38
39
|
while [ "$#" -gt 0 ]; do
|
|
@@ -44,8 +45,9 @@ while [ "$#" -gt 0 ]; do
|
|
|
44
45
|
--ext-id) require_value "$@"; EXT_ID="$2"; shift 2 ;;
|
|
45
46
|
--dist-dir) require_value "$@"; DIST_DIR="$2"; shift 2 ;;
|
|
46
47
|
--settle-ms) require_value "$@"; SETTLE_MS="$2"; shift 2 ;;
|
|
48
|
+
--display-mode) require_value "$@"; DISPLAY_MODE="$2"; shift 2 ;;
|
|
47
49
|
-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>]"
|
|
50
|
+
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>] [--display-mode fullscreen|sidepanel]"
|
|
49
51
|
exit 0
|
|
50
52
|
;;
|
|
51
53
|
*) echo "reattach: unknown arg: $1" >&2; exit 2 ;;
|
|
@@ -60,6 +62,10 @@ fi
|
|
|
60
62
|
case "$CDP_PORT" in
|
|
61
63
|
*[!0-9]*) echo "reattach: --cdp-port must be numeric (got: $CDP_PORT)" >&2; exit 2 ;;
|
|
62
64
|
esac
|
|
65
|
+
case "$DISPLAY_MODE" in
|
|
66
|
+
fullscreen|sidepanel) ;;
|
|
67
|
+
*) echo "reattach: --display-mode must be fullscreen or sidepanel (got: $DISPLAY_MODE)" >&2; exit 2 ;;
|
|
68
|
+
esac
|
|
63
69
|
|
|
64
70
|
if [ ! -f "$TARGET/package.json" ]; then
|
|
65
71
|
echo "reattach: --target is not a checkout (no package.json): $TARGET" >&2
|
|
@@ -75,23 +81,33 @@ if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; t
|
|
|
75
81
|
fi
|
|
76
82
|
|
|
77
83
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
84
|
+
# shellcheck disable=SC1091
|
|
85
|
+
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
|
|
86
|
+
[ -f "$_hp" ] && { . "$_hp"; break; }
|
|
87
|
+
done
|
|
88
|
+
unset _hp
|
|
78
89
|
DIST_ABS="$TARGET/$DIST_DIR"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
if command -v recipe_runtime_dir >/dev/null 2>&1; then
|
|
91
|
+
RUNTIME_DIR="$(recipe_runtime_dir)"
|
|
92
|
+
else
|
|
93
|
+
RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
|
|
94
|
+
fi
|
|
95
|
+
RUNTIME_DIST_DIR="${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}"
|
|
96
|
+
# The running Chrome loads this unpacked extension directory. Do not recreate or
|
|
97
|
+
# rsync-delete it while Chrome is live: Chrome can keep the intended
|
|
98
|
+
# chrome-extension:// target URL but serve chrome-error://chromewebdata after its
|
|
99
|
+
# loaded directory is replaced underneath it. Fresh snapshots belong to the clean
|
|
100
|
+
# relaunch path; quick reattach only reloads the already-loaded runtime.
|
|
101
|
+
RUNTIME_DIST_ABS="$TARGET/$RUNTIME_DIR/$RUNTIME_DIST_DIR"
|
|
102
|
+
echo "[reattach] reusing loaded runtime-dist: $RUNTIME_DIST_ABS" >&2
|
|
103
|
+
if [ -d "$DIST_ABS" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
|
|
104
|
+
echo "[reattach] refreshing loaded runtime-dist in place" >&2
|
|
105
|
+
rsync -a --exclude _metadata "$DIST_ABS/" "$RUNTIME_DIST_ABS/" >&2
|
|
90
106
|
fi
|
|
91
107
|
|
|
92
108
|
# Resolve the extension id: explicit flag, then the recorded id file, else CDP.
|
|
93
109
|
if [ -z "$EXT_ID" ]; then
|
|
94
|
-
for idf in "$TARGET/
|
|
110
|
+
for idf in "$TARGET/$RUNTIME_DIR/extension.id" "$SCRIPT_DIR/extension.id"; do
|
|
95
111
|
if [ -f "$idf" ]; then
|
|
96
112
|
EXT_ID="$(tr -d '[:space:]' < "$idf")"
|
|
97
113
|
[ -n "$EXT_ID" ] && break
|
|
@@ -102,23 +118,82 @@ fi
|
|
|
102
118
|
cd "$TARGET"
|
|
103
119
|
echo "[reattach] reloading extension in place over CDP :${CDP_PORT}" >&2
|
|
104
120
|
|
|
105
|
-
CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" START_URL="$START_URL" SETTLE_MS="$SETTLE_MS" node <<'NODE'
|
|
121
|
+
CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" TARGET="$TARGET" SCRIPT_DIR="$SCRIPT_DIR" RUNTIME_DIR="$RUNTIME_DIR" RUNTIME_DIST_DIR="$RUNTIME_DIST_DIR" START_URL="$START_URL" SETTLE_MS="$SETTLE_MS" DISPLAY_MODE="$DISPLAY_MODE" node <<'NODE'
|
|
106
122
|
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
123
|
+
const fs = require('node:fs');
|
|
124
|
+
const path = require('node:path');
|
|
125
|
+
const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
|
|
107
126
|
|
|
108
127
|
const port = process.env.CDP_PORT;
|
|
128
|
+
const target = process.env.TARGET;
|
|
129
|
+
const runtimeDir = process.env.RUNTIME_DIR;
|
|
130
|
+
const runtimeDistDir = process.env.RUNTIME_DIST_DIR;
|
|
109
131
|
const startUrl = process.env.START_URL || '';
|
|
110
132
|
const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
|
|
133
|
+
const displayMode = process.env.DISPLAY_MODE || 'fullscreen';
|
|
134
|
+
const slotId = process.env.RECIPE_SLOT_ID || process.env.SLOT_ID || process.env.FARMSLOT_SLOT_ID || readRuntimeSlotId();
|
|
135
|
+
|
|
136
|
+
if (!runtimeDir || !runtimeDistDir) {
|
|
137
|
+
console.error('reattach: resolved runtime paths were not provided.');
|
|
138
|
+
process.exit(2);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readRuntimeSlotId() {
|
|
142
|
+
try {
|
|
143
|
+
const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, runtimeDir, 'agentic-runtime.json'), 'utf8'));
|
|
144
|
+
return typeof runtimeContext.slotId === 'string' ? runtimeContext.slotId.trim() : '';
|
|
145
|
+
} catch {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extensionIdFromRuntimeDist() {
|
|
151
|
+
return extensionIdFromManifestFile(path.join(target, runtimeDir, runtimeDistDir, 'manifest.json'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validExtensionId(value) {
|
|
155
|
+
return /^[a-p]{32}$/u.test(String(value || '')) ? String(value) : '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function writeExtensionId(id) {
|
|
159
|
+
if (!validExtensionId(id)) return;
|
|
160
|
+
try {
|
|
161
|
+
const idPath = path.join(target, runtimeDir, 'extension.id');
|
|
162
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
163
|
+
fs.writeFileSync(idPath, `${id}\n`);
|
|
164
|
+
} catch {
|
|
165
|
+
// Best-effort marker repair; the live CDP reload is the source of truth.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
111
168
|
|
|
112
169
|
function extensionIdFrom(context, provided) {
|
|
113
|
-
if (provided) return provided;
|
|
114
170
|
const urls = [];
|
|
115
|
-
|
|
116
|
-
for (const
|
|
171
|
+
const ids = [];
|
|
172
|
+
for (const page of allPages(context)) urls.push(page.url());
|
|
173
|
+
for (const worker of allServiceWorkers(context)) urls.push(worker.url());
|
|
117
174
|
for (const url of urls) {
|
|
118
175
|
const match = /^chrome-extension:\/\/([^/]+)\//u.exec(url);
|
|
119
|
-
if (match
|
|
176
|
+
if (match && !ids.includes(match[1])) ids.push(match[1]);
|
|
177
|
+
}
|
|
178
|
+
if (provided) {
|
|
179
|
+
if (ids.length === 0 || ids.includes(provided)) return provided;
|
|
180
|
+
return '';
|
|
120
181
|
}
|
|
121
|
-
return '';
|
|
182
|
+
return ids[0] || '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function allContexts(context) {
|
|
186
|
+
const browser = context.browser?.();
|
|
187
|
+
const contexts = browser?.contexts?.();
|
|
188
|
+
return contexts && contexts.length ? contexts : [context];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function allPages(context) {
|
|
192
|
+
return allContexts(context).flatMap((candidate) => candidate.pages());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function allServiceWorkers(context) {
|
|
196
|
+
return allContexts(context).flatMap((candidate) => candidate.serviceWorkers());
|
|
122
197
|
}
|
|
123
198
|
|
|
124
199
|
async function connect() {
|
|
@@ -127,20 +202,56 @@ async function connect() {
|
|
|
127
202
|
return { browser, context };
|
|
128
203
|
}
|
|
129
204
|
|
|
205
|
+
async function stampSlotTitle(page) {
|
|
206
|
+
if (!slotId || !page) return;
|
|
207
|
+
await page.evaluate((slot) => {
|
|
208
|
+
const base = (document.title || 'MetaMask').replace(/^.+?\s+—\s+/, '') || 'MetaMask';
|
|
209
|
+
const next = `${slot} — ${base}`;
|
|
210
|
+
if (document.title !== next) document.title = next;
|
|
211
|
+
}, slotId).catch(() => {
|
|
212
|
+
// Best-effort operator affordance; reload success is verified by CDP state.
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function closeDisposableChromeTabs(context) {
|
|
217
|
+
for (const page of allPages(context)) {
|
|
218
|
+
const url = page.url();
|
|
219
|
+
if (url === 'chrome://newtab/' || url === 'chrome://new-tab-page/' || url === 'chrome://extensions/') {
|
|
220
|
+
await page.close().catch(() => {
|
|
221
|
+
// Best-effort tab hygiene; runtime health is verified separately.
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function closePages(context, predicate) {
|
|
228
|
+
for (const page of allPages(context)) {
|
|
229
|
+
if (!predicate(page)) continue;
|
|
230
|
+
await page.close().catch(() => {
|
|
231
|
+
// Best-effort display-mode cleanup; final health checks own correctness.
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
130
236
|
(async () => {
|
|
131
|
-
// Phase 1:
|
|
132
|
-
//
|
|
237
|
+
// Phase 1: attach to the already-loaded extension page. Do not call
|
|
238
|
+
// chrome.runtime.reload() here: on the slot Chrome version it can leave the
|
|
239
|
+
// target URL as chrome-extension://.../home.html while the actual document is
|
|
240
|
+
// chrome-error://chromewebdata. Clean relaunch owns loading a new unpacked
|
|
241
|
+
// runtime-dist; quick reattach only refreshes/foregrounds the live UI.
|
|
133
242
|
let { browser, context } = await connect();
|
|
134
|
-
const
|
|
243
|
+
const providedExtId = extensionIdFromRuntimeDist() || validExtensionId(process.env.EXT_ID);
|
|
244
|
+
const extId = extensionIdFrom(context, providedExtId);
|
|
135
245
|
if (!extId) {
|
|
136
|
-
await browser.
|
|
137
|
-
|
|
246
|
+
if (typeof browser.disconnect === 'function') await browser.disconnect();
|
|
247
|
+
else await browser.close();
|
|
248
|
+
console.error('reattach: could not resolve the extension id from runtime-dist or live CDP targets.');
|
|
138
249
|
console.error(' Next: mm-harness launch --build (clean build + relaunch)');
|
|
139
250
|
process.exit(1);
|
|
140
251
|
}
|
|
252
|
+
writeExtensionId(extId);
|
|
141
253
|
|
|
142
|
-
let page = context
|
|
143
|
-
.pages()
|
|
254
|
+
let page = allPages(context)
|
|
144
255
|
.find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`));
|
|
145
256
|
if (!page) {
|
|
146
257
|
page = await context.newPage();
|
|
@@ -149,35 +260,19 @@ async function connect() {
|
|
|
149
260
|
timeout: 15000,
|
|
150
261
|
});
|
|
151
262
|
}
|
|
152
|
-
await page.
|
|
153
|
-
|
|
263
|
+
await page.reload({ waitUntil: 'domcontentloaded', timeout: 15000 }).catch(async () => {
|
|
264
|
+
await page.goto(`chrome-extension://${extId}/home.html`, {
|
|
265
|
+
waitUntil: 'domcontentloaded',
|
|
266
|
+
timeout: 15000,
|
|
267
|
+
});
|
|
154
268
|
});
|
|
155
|
-
await browser.
|
|
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
|
-
}
|
|
269
|
+
if (typeof browser.disconnect === 'function') await browser.disconnect();
|
|
270
|
+
else await browser.close();
|
|
177
271
|
|
|
272
|
+
// Phase 2: re-open/retitle the wallet and, when requested, a dapp tab. Chrome
|
|
273
|
+
// itself never restarted, so CDP stays up.
|
|
178
274
|
({ browser, context } = await connect());
|
|
179
|
-
let home = context
|
|
180
|
-
.pages()
|
|
275
|
+
let home = allPages(context)
|
|
181
276
|
.find(
|
|
182
277
|
(candidate) =>
|
|
183
278
|
candidate.url().startsWith(`chrome-extension://${extId}/`) &&
|
|
@@ -189,16 +284,29 @@ async function connect() {
|
|
|
189
284
|
.goto(`chrome-extension://${extId}/home.html`, { waitUntil: 'domcontentloaded', timeout: 15000 })
|
|
190
285
|
.catch(() => { /* wallet home is best-effort; the reload already applied */ });
|
|
191
286
|
}
|
|
287
|
+
await stampSlotTitle(home);
|
|
288
|
+
await closeDisposableChromeTabs(context);
|
|
192
289
|
if (startUrl) {
|
|
193
|
-
const dapp = context
|
|
194
|
-
.pages()
|
|
290
|
+
const dapp = allPages(context)
|
|
195
291
|
.find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
|
|
196
292
|
const tab = dapp || (await context.newPage());
|
|
197
293
|
await tab
|
|
198
294
|
.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
|
199
295
|
.catch(() => { /* dapp navigation is best-effort */ });
|
|
200
296
|
}
|
|
201
|
-
|
|
297
|
+
if (displayMode === 'fullscreen') {
|
|
298
|
+
await closePages(context, (candidate) => {
|
|
299
|
+
const url = candidate.url();
|
|
300
|
+
if (url.startsWith(`chrome-extension://${extId}/`) && url.includes('/sidepanel.html')) return true;
|
|
301
|
+
if (!startUrl && (url.startsWith('http://') || url.startsWith('https://'))) return true;
|
|
302
|
+
return false;
|
|
303
|
+
});
|
|
304
|
+
await home.bringToFront().catch(() => {
|
|
305
|
+
// Foreground is an operator affordance; health is verified separately.
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (typeof browser.disconnect === 'function') await browser.disconnect();
|
|
309
|
+
else await browser.close();
|
|
202
310
|
})().catch((error) => {
|
|
203
311
|
console.error(`reattach: ${error && error.message ? error.message : error}`);
|
|
204
312
|
console.error(' Next: mm-harness launch --build (clean build + relaunch)');
|