@deeeed/metamask-harness 0.6.2 → 0.6.3
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 +5 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/reattach.sh +210 -0
- package/adapters/manifest.json +8 -0
- package/dist/adapters/extension/runtime-decision.js +4 -0
- package/dist/commands/launch/extension.js +62 -6
- package/dist/mm-harness-cli.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.3
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Extension `launch` reuses a healthy live runtime instead of always clean-building** — quick `launch` / `launch --sidepanel` on an extension slot now probes for a reusable runtime (webpack watcher LISTENing on `WATCHER_PORT` + CDP `/json/version` reachable on `CDP_PORT` + dist not stale). When all hold it REATTACHES: re-snapshots `dist/chrome` into the loaded runtime-dist and reloads the extension in place over CDP (service worker + pages), re-asserts the side panel, and applies `--url` — against the existing slot Chrome profile, with no watcher stop, webpack cache clear, or Chrome kill/relaunch. Reuse is best-effort: a failed probe (watcher down, CDP unreachable, stale dist) falls through to the clean path, and `--build` is the explicit escape that always clean-builds + relaunches a fresh runtime (no probe) to force-recover a broken slot.
|
|
7
|
+
|
|
3
8
|
## 0.6.2 - 2026-07-06
|
|
4
9
|
|
|
5
10
|
### Added
|
|
@@ -101,6 +101,7 @@ copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(
|
|
|
101
101
|
copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
|
|
102
102
|
copyFile(path.join(runnerDir, 'adapters/extension/seed-fixture.sh'), path.join(harnessDir, 'scripts/seed-fixture.sh'));
|
|
103
103
|
copyFile(path.join(runnerDir, 'adapters/extension/sidepanel-toggle.sh'), path.join(harnessDir, 'scripts/sidepanel-toggle.sh'));
|
|
104
|
+
copyFile(path.join(runnerDir, 'adapters/extension/reattach.sh'), path.join(harnessDir, 'scripts/reattach.sh'));
|
|
104
105
|
copyFile(path.join(runnerDir, 'adapters/extension/readiness.mjs'), path.join(harnessDir, 'scripts/readiness.mjs'));
|
|
105
106
|
copyFile(path.join(runnerDir, 'adapters/extension/wallet-fixture-state.cjs'), path.join(harnessDir, 'scripts/wallet-fixture-state.cjs'));
|
|
106
107
|
copyFile(path.join(runnerDir, 'adapters/extension/verify.sh'), path.join(harnessDir, 'scripts/verify.sh'));
|
|
@@ -138,6 +139,7 @@ const manifest = {
|
|
|
138
139
|
refreshBuild: `${harnessRel}/scripts/refresh-build.sh`,
|
|
139
140
|
reopenBrowser: `${harnessRel}/scripts/reopen-browser.sh`,
|
|
140
141
|
sidepanelToggle: `${harnessRel}/scripts/sidepanel-toggle.sh`,
|
|
142
|
+
reattach: `${harnessRel}/scripts/reattach.sh`,
|
|
141
143
|
},
|
|
142
144
|
patchedFiles: [],
|
|
143
145
|
backupDir: null,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# reattach.sh — reload a live extension slot in place (no rebuild, no relaunch)
|
|
3
|
+
#
|
|
4
|
+
# Purpose:
|
|
5
|
+
# Quick-reuse leaf for `mm-harness launch` on an already-healthy extension slot:
|
|
6
|
+
# re-snapshots dist/chrome into the loaded runtime-dist so the running Chrome
|
|
7
|
+
# picks up watched code changes, then reloads the extension over CDP (service
|
|
8
|
+
# worker + extension pages) and re-opens the wallet. Optionally navigates a
|
|
9
|
+
# main tab to --start-url. Runs entirely against the EXISTING slot Chrome
|
|
10
|
+
# profile — it never stops the watcher, kills Chrome, or clears the webpack
|
|
11
|
+
# cache (that is the clean path in live.sh).
|
|
12
|
+
#
|
|
13
|
+
# Inputs (flags / env):
|
|
14
|
+
# --target <metamask-extension> (default $PWD; becomes the working dir)
|
|
15
|
+
# --cdp-port <port> (required, numeric) — the slot's CDP endpoint
|
|
16
|
+
# --watcher-port <port> (optional; informational only)
|
|
17
|
+
# --start-url <url> (optional; opened as a main tab beside the wallet)
|
|
18
|
+
# --ext-id <id> (optional; else resolved from the extension.id file or CDP)
|
|
19
|
+
# --dist-dir <rel> (default dist/chrome), --settle-ms <ms> (default 8000)
|
|
20
|
+
#
|
|
21
|
+
# Outputs:
|
|
22
|
+
# Progress on stderr. Exit 0 — reloaded; 1 — repo/reload failed; 2 — bad args
|
|
23
|
+
# / non-numeric port / CDP unreachable.
|
|
24
|
+
#
|
|
25
|
+
# Never touches: product source files; the webpack watcher; the Chrome process;
|
|
26
|
+
# other slots' browsers (single CDP port scope).
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
TARGET="$PWD"
|
|
30
|
+
CDP_PORT="${CDP_PORT:-}"
|
|
31
|
+
WATCHER_PORT="${WATCHER_PORT:-}"
|
|
32
|
+
START_URL="${EXTENSION_START_URL:-}"
|
|
33
|
+
EXT_ID="${EXT_ID:-}"
|
|
34
|
+
DIST_DIR="dist/chrome"
|
|
35
|
+
SETTLE_MS="8000"
|
|
36
|
+
|
|
37
|
+
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
|
|
38
|
+
while [ "$#" -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--target) require_value "$@"; TARGET="$2"; shift 2 ;;
|
|
41
|
+
--cdp-port) require_value "$@"; CDP_PORT="$2"; shift 2 ;;
|
|
42
|
+
--watcher-port) require_value "$@"; WATCHER_PORT="$2"; shift 2 ;;
|
|
43
|
+
--start-url) require_value "$@"; START_URL="$2"; shift 2 ;;
|
|
44
|
+
--ext-id) require_value "$@"; EXT_ID="$2"; shift 2 ;;
|
|
45
|
+
--dist-dir) require_value "$@"; DIST_DIR="$2"; shift 2 ;;
|
|
46
|
+
--settle-ms) require_value "$@"; SETTLE_MS="$2"; shift 2 ;;
|
|
47
|
+
-h|--help)
|
|
48
|
+
echo "Usage: reattach.sh --target <metamask-extension> --cdp-port <port> [--watcher-port <port>] [--start-url <url>] [--ext-id <id>] [--dist-dir <rel>] [--settle-ms <ms>]"
|
|
49
|
+
exit 0
|
|
50
|
+
;;
|
|
51
|
+
*) echo "reattach: unknown arg: $1" >&2; exit 2 ;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
if [ -z "$CDP_PORT" ]; then
|
|
56
|
+
echo "reattach: --cdp-port is required" >&2
|
|
57
|
+
echo " Next: mm-harness launch --build (clean build + relaunch a fresh slot)" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
case "$CDP_PORT" in
|
|
61
|
+
*[!0-9]*) echo "reattach: --cdp-port must be numeric (got: $CDP_PORT)" >&2; exit 2 ;;
|
|
62
|
+
esac
|
|
63
|
+
|
|
64
|
+
if [ ! -f "$TARGET/package.json" ]; then
|
|
65
|
+
echo "reattach: --target is not a checkout (no package.json): $TARGET" >&2
|
|
66
|
+
echo " Next: mm-harness launch --target /path/to/metamask-extension --build" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
TARGET="$(cd "$TARGET" && pwd)"
|
|
70
|
+
|
|
71
|
+
if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
|
|
72
|
+
echo "reattach: CDP not reachable on port ${CDP_PORT}; the slot browser is not live." >&2
|
|
73
|
+
echo " Next: mm-harness launch --build (clean build + relaunch)" >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
78
|
+
DIST_ABS="$TARGET/$DIST_DIR"
|
|
79
|
+
# The running Chrome loads the runtime-dist snapshot (frozen at launch), not the
|
|
80
|
+
# live dist. Re-snapshot so the in-place reload serves the latest watched build.
|
|
81
|
+
RUNTIME_DIST_ABS="$TARGET/temp/recipe/runtime/runtime-dist"
|
|
82
|
+
|
|
83
|
+
if [ -f "$SCRIPT_DIR/snapshot-dist.sh" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
|
|
84
|
+
echo "[reattach] re-snapshotting dist -> runtime-dist for in-place reload" >&2
|
|
85
|
+
bash "$SCRIPT_DIR/snapshot-dist.sh" --dist "$DIST_ABS" --runtime-dist "$RUNTIME_DIST_ABS" >&2 || {
|
|
86
|
+
echo "reattach: dist re-snapshot failed; the loaded extension may be mid-rebuild." >&2
|
|
87
|
+
echo " Next: mm-harness launch --build (clean build + relaunch)" >&2
|
|
88
|
+
exit 1
|
|
89
|
+
}
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Resolve the extension id: explicit flag, then the recorded id file, else CDP.
|
|
93
|
+
if [ -z "$EXT_ID" ]; then
|
|
94
|
+
for idf in "$TARGET/temp/recipe/runtime/extension.id" "$SCRIPT_DIR/extension.id"; do
|
|
95
|
+
if [ -f "$idf" ]; then
|
|
96
|
+
EXT_ID="$(tr -d '[:space:]' < "$idf")"
|
|
97
|
+
[ -n "$EXT_ID" ] && break
|
|
98
|
+
fi
|
|
99
|
+
done
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
cd "$TARGET"
|
|
103
|
+
echo "[reattach] reloading extension in place over CDP :${CDP_PORT}" >&2
|
|
104
|
+
|
|
105
|
+
CDP_PORT="$CDP_PORT" EXT_ID="$EXT_ID" START_URL="$START_URL" SETTLE_MS="$SETTLE_MS" node <<'NODE'
|
|
106
|
+
let chromium; try { chromium = require('@playwright/test').chromium; } catch { chromium = require('playwright').chromium; }
|
|
107
|
+
|
|
108
|
+
const port = process.env.CDP_PORT;
|
|
109
|
+
const startUrl = process.env.START_URL || '';
|
|
110
|
+
const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
|
|
111
|
+
|
|
112
|
+
function extensionIdFrom(context, provided) {
|
|
113
|
+
if (provided) return provided;
|
|
114
|
+
const urls = [];
|
|
115
|
+
for (const page of context.pages()) urls.push(page.url());
|
|
116
|
+
for (const worker of context.serviceWorkers()) urls.push(worker.url());
|
|
117
|
+
for (const url of urls) {
|
|
118
|
+
const match = /^chrome-extension:\/\/([^/]+)\//u.exec(url);
|
|
119
|
+
if (match) return match[1];
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function connect() {
|
|
125
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
126
|
+
const context = browser.contexts()[0];
|
|
127
|
+
return { browser, context };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
(async () => {
|
|
131
|
+
// Phase 1: trigger a full extension reload (service worker + all pages) from an
|
|
132
|
+
// extension page so Chrome re-reads the freshly snapshotted runtime-dist.
|
|
133
|
+
let { browser, context } = await connect();
|
|
134
|
+
const extId = extensionIdFrom(context, process.env.EXT_ID);
|
|
135
|
+
if (!extId) {
|
|
136
|
+
await browser.close();
|
|
137
|
+
console.error('reattach: could not resolve the extension id from the live CDP targets.');
|
|
138
|
+
console.error(' Next: mm-harness launch --build (clean build + relaunch)');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let page = context
|
|
143
|
+
.pages()
|
|
144
|
+
.find((candidate) => candidate.url().startsWith(`chrome-extension://${extId}/`));
|
|
145
|
+
if (!page) {
|
|
146
|
+
page = await context.newPage();
|
|
147
|
+
await page.goto(`chrome-extension://${extId}/home.html`, {
|
|
148
|
+
waitUntil: 'domcontentloaded',
|
|
149
|
+
timeout: 15000,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
await page.evaluate(() => chrome.runtime.reload()).catch(() => {
|
|
153
|
+
// The reload tears the page context down; a rejected evaluate is expected.
|
|
154
|
+
});
|
|
155
|
+
await browser.close();
|
|
156
|
+
|
|
157
|
+
// Phase 2: wait for the extension to come back, then re-open the wallet and,
|
|
158
|
+
// when requested, a dapp tab. Chrome itself never restarted, so CDP stays up.
|
|
159
|
+
const deadline = Date.now() + settleMs;
|
|
160
|
+
let back = false;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
try {
|
|
163
|
+
const probe = await connect();
|
|
164
|
+
const id = extensionIdFrom(probe.context, extId);
|
|
165
|
+
await probe.browser.close();
|
|
166
|
+
if (id) { back = true; break; }
|
|
167
|
+
} catch {
|
|
168
|
+
// CDP momentarily unavailable during reload; retry until the deadline.
|
|
169
|
+
}
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
171
|
+
}
|
|
172
|
+
if (!back) {
|
|
173
|
+
console.error(`reattach: extension did not come back within ${settleMs}ms after reload.`);
|
|
174
|
+
console.error(' Next: mm-harness launch --build (clean build + relaunch)');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
({ browser, context } = await connect());
|
|
179
|
+
let home = context
|
|
180
|
+
.pages()
|
|
181
|
+
.find(
|
|
182
|
+
(candidate) =>
|
|
183
|
+
candidate.url().startsWith(`chrome-extension://${extId}/`) &&
|
|
184
|
+
!candidate.url().includes('/sidepanel.html'),
|
|
185
|
+
);
|
|
186
|
+
if (!home) {
|
|
187
|
+
home = await context.newPage();
|
|
188
|
+
await home
|
|
189
|
+
.goto(`chrome-extension://${extId}/home.html`, { waitUntil: 'domcontentloaded', timeout: 15000 })
|
|
190
|
+
.catch(() => { /* wallet home is best-effort; the reload already applied */ });
|
|
191
|
+
}
|
|
192
|
+
if (startUrl) {
|
|
193
|
+
const dapp = context
|
|
194
|
+
.pages()
|
|
195
|
+
.find((candidate) => candidate.url().startsWith('http://') || candidate.url().startsWith('https://'));
|
|
196
|
+
const tab = dapp || (await context.newPage());
|
|
197
|
+
await tab
|
|
198
|
+
.goto(startUrl, { waitUntil: 'domcontentloaded', timeout: 20000 })
|
|
199
|
+
.catch(() => { /* dapp navigation is best-effort */ });
|
|
200
|
+
}
|
|
201
|
+
await browser.close();
|
|
202
|
+
})().catch((error) => {
|
|
203
|
+
console.error(`reattach: ${error && error.message ? error.message : error}`);
|
|
204
|
+
console.error(' Next: mm-harness launch --build (clean build + relaunch)');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
NODE
|
|
208
|
+
|
|
209
|
+
echo "[reattach] extension reloaded in place — no rebuild, no relaunch" >&2
|
|
210
|
+
echo " Next: mm-harness verify (confirm the reloaded runtime)" >&2
|
package/adapters/manifest.json
CHANGED
|
@@ -178,6 +178,14 @@
|
|
|
178
178
|
"inputs": "<status|open|close|toggle|cycle> --cdp-port --ext-id --agent-dir --settle-ms; env REPO, CDP_PORT, EXT_ID, SETTLE_MS, AGENT_DIR",
|
|
179
179
|
"outputs": "status/progress on stdout; exit 0/1/2"
|
|
180
180
|
},
|
|
181
|
+
{
|
|
182
|
+
"id": "extension/reattach",
|
|
183
|
+
"entry": "adapters/extension/reattach.sh",
|
|
184
|
+
"kind": "bash",
|
|
185
|
+
"purpose": "Reload a live extension slot in place: re-snapshot dist into the loaded runtime-dist, then reload the extension (service worker + pages) over CDP — no watcher stop, no Chrome kill.",
|
|
186
|
+
"inputs": "--target --cdp-port --watcher-port --start-url --ext-id --dist-dir --settle-ms; env CDP_PORT, WATCHER_PORT, EXTENSION_START_URL, EXT_ID",
|
|
187
|
+
"outputs": "progress on stderr; exit 0/1/2"
|
|
188
|
+
},
|
|
181
189
|
{
|
|
182
190
|
"id": "extension/pin-remote-flags",
|
|
183
191
|
"entry": "adapters/extension/pin-remote-flags.cjs",
|
|
@@ -181,6 +181,9 @@ function distCheck(target) {
|
|
|
181
181
|
}
|
|
182
182
|
return { status: "fresh", distGitId: distShort, head: headShort };
|
|
183
183
|
}
|
|
184
|
+
function isExtensionDistStale(target) {
|
|
185
|
+
return distCheck(path.resolve(target)).status === "stale";
|
|
186
|
+
}
|
|
184
187
|
async function cdpCheck(target, cdpPort) {
|
|
185
188
|
if (!cdpPort) return { status: "skipped" };
|
|
186
189
|
try {
|
|
@@ -301,5 +304,6 @@ function buildAction(target, clean) {
|
|
|
301
304
|
}
|
|
302
305
|
export {
|
|
303
306
|
decideExtensionReadiness,
|
|
307
|
+
isExtensionDistStale,
|
|
304
308
|
recordReadinessBaseline
|
|
305
309
|
};
|
|
@@ -1,17 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import http from "node:http";
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
6
|
+
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
4
7
|
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
5
8
|
import { spawnScriptStreaming } from "../shared.js";
|
|
6
9
|
async function launchExtension(target, tier, wantWatch) {
|
|
7
|
-
if (
|
|
10
|
+
if (wantWatch) {
|
|
11
|
+
const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
|
|
12
|
+
const watchArgs = ["--target", target];
|
|
13
|
+
if (process.env.WATCHER_PORT) watchArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
14
|
+
console.error(`\u2192 extension watch \u2014 webpack :${process.env.WATCHER_PORT ?? "default"} (output streams below)`);
|
|
15
|
+
return spawnScriptStreaming(startWatchSh, watchArgs, target);
|
|
16
|
+
}
|
|
17
|
+
if (tier === "build") {
|
|
8
18
|
return extensionRebuild(target);
|
|
9
19
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
if (await extensionRuntimeReusable(target)) {
|
|
21
|
+
return extensionReattach(target);
|
|
22
|
+
}
|
|
23
|
+
return extensionRebuild(target);
|
|
24
|
+
}
|
|
25
|
+
async function extensionRuntimeReusable(target) {
|
|
26
|
+
const watcherPort = process.env.WATCHER_PORT;
|
|
27
|
+
const cdpPort = process.env.CDP_PORT;
|
|
28
|
+
if (!watcherPort || !cdpPort) return false;
|
|
29
|
+
if (!portHasListener(watcherPort)) return false;
|
|
30
|
+
if (!await cdpVersionReachable(cdpPort)) return false;
|
|
31
|
+
if (isExtensionDistStale(target)) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function portHasListener(port) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
39
|
+
});
|
|
40
|
+
return out.split(/\s+/u).some((value) => /^\d+$/u.test(value));
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function cdpVersionReachable(port) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const request = http.get(
|
|
48
|
+
{ host: "127.0.0.1", port: Number(port), path: "/json/version", timeout: 2e3 },
|
|
49
|
+
(response) => {
|
|
50
|
+
response.resume();
|
|
51
|
+
const code = response.statusCode ?? 0;
|
|
52
|
+
resolve(code >= 200 && code < 300);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
request.on("timeout", () => {
|
|
56
|
+
request.destroy();
|
|
57
|
+
resolve(false);
|
|
58
|
+
});
|
|
59
|
+
request.on("error", () => resolve(false));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function extensionReattach(target) {
|
|
63
|
+
const reattachScript = recipeHarnessPath(target, "extension", "scripts", "reattach.sh");
|
|
64
|
+
const reattachArgs = ["--target", target];
|
|
65
|
+
if (process.env.CDP_PORT) reattachArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
66
|
+
if (process.env.WATCHER_PORT) reattachArgs.push("--watcher-port", process.env.WATCHER_PORT);
|
|
67
|
+
if (process.env.EXTENSION_START_URL) reattachArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
68
|
+
console.error(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`);
|
|
69
|
+
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
15
70
|
}
|
|
16
71
|
async function extensionRebuild(target) {
|
|
17
72
|
const runtimeDirRel = recipeRuntimeDir();
|
|
@@ -35,6 +90,7 @@ async function extensionRebuild(target) {
|
|
|
35
90
|
return result;
|
|
36
91
|
}
|
|
37
92
|
export {
|
|
93
|
+
extensionReattach,
|
|
38
94
|
extensionRebuild,
|
|
39
95
|
launchExtension
|
|
40
96
|
};
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -230,8 +230,10 @@ Example:
|
|
|
230
230
|
Launch the app \u2014 quick relaunch by default; --build for a full native/webpack build.
|
|
231
231
|
Mobile: target is MANDATORY (ios | android). Extension: no target (--fullscreen default;
|
|
232
232
|
--sidepanel to override). core is headless (teaching error \u2014 use verify/run).
|
|
233
|
+
Extension quick launch REUSES a healthy live runtime (reload in place, no rebuild);
|
|
234
|
+
--build is the escape hatch that always clean-builds + relaunches a fresh runtime.
|
|
233
235
|
|
|
234
|
-
--build Full
|
|
236
|
+
--build Full clean build + relaunch (escape hatch; skips extension reuse)
|
|
235
237
|
--verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
|
|
236
238
|
--runway Post-launch runway check (mobile only; teaching error elsewhere)
|
|
237
239
|
--watch Persistent webpack watcher then relaunch (extension only)
|