@deeeed/metamask-harness 0.50.0 → 0.50.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.50.1 - 2026-09-08
6
+
7
+ - Hand Extension builds to a fresh background worker before readiness, including watched edits and quick reloads, while preserving wallet profiles and native sidepanels. Refuse development copies over release artifacts.
8
+
5
9
  ## 0.50.0 - 2026-09-08
6
10
 
7
11
  - Bind Mobile order assertions and cleanup to accepted receipts, exact TP/SL values and complete snapshots. iOS passed one limit cancellation and one protected-market close; protected limits, full recipes, CUF and Android remain unvalidated.
@@ -0,0 +1,198 @@
1
+ // Replace an unpacked build while its extension is disabled, preserving storage and native panels.
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { connectBrowserViaCdp, evaluatePageViaCdp } = require('./lib/playwright-cdp.cjs');
8
+ const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = require('./lib/chrome-args.cjs');
9
+ const { stampRuntimeTitles } = require('./stamp-runtime-title.cjs');
10
+
11
+ function acquireBuildHandoff(runtimeDir) {
12
+ const lock = path.join(runtimeDir, 'extension-build-handoff.lock');
13
+ const deadline = Date.now() + 30_000;
14
+ while (true) {
15
+ try {
16
+ const descriptor = fs.openSync(lock, 'wx', 0o600);
17
+ fs.writeFileSync(descriptor, String(process.pid));
18
+ fs.closeSync(descriptor);
19
+ return () => fs.rmSync(lock, { force: true });
20
+ } catch (error) {
21
+ if (error.code !== 'EEXIST') throw error;
22
+ let owner;
23
+ try { owner = Number(fs.readFileSync(lock, 'utf8')); } catch (readError) {
24
+ if (readError.code === 'ENOENT') continue;
25
+ throw readError;
26
+ }
27
+ if (Number.isInteger(owner) && owner > 0) {
28
+ try { process.kill(owner, 0); } catch (ownerError) {
29
+ if (ownerError.code !== 'ESRCH') throw ownerError;
30
+ fs.rmSync(lock, { force: true });
31
+ continue;
32
+ }
33
+ }
34
+ if (Date.now() >= deadline) throw new Error('Extension build handoff is busy. Next: wait for the current launch to finish.');
35
+ spawnSync('sleep', ['0.1']);
36
+ }
37
+ }
38
+ }
39
+
40
+ async function sidePanelContexts(context, url, windowIds = []) {
41
+ const page = await context.newPage();
42
+ const session = await context.newCDPSession(page);
43
+ const key = 'mm-harness:build-handoff:side-panel';
44
+ let preload;
45
+ try {
46
+ await session.send('Page.enable');
47
+ // Capture Chrome APIs before LavaMoat scuttles globals in the extension page.
48
+ preload = await session.send('Page.addScriptToEvaluateOnNewDocument', {
49
+ source: `(() => {
50
+ const read = chrome.runtime.getContexts.bind(chrome.runtime);
51
+ const open = chrome.sidePanel.open.bind(chrome.sidePanel);
52
+ const listWindows = chrome.windows.getAll.bind(chrome.windows);
53
+ globalThis[Symbol.for(${JSON.stringify(key)})] = async (windowIds) => {
54
+ await Promise.all(windowIds.map(windowId => open({ windowId })));
55
+ const [panels, windows] = await Promise.all([read({ contextTypes: ['SIDE_PANEL'] }), listWindows({ windowTypes: ['normal'] })]);
56
+ // Chromium can omit a panel's window ID; only one normal window is unambiguous.
57
+ return panels.map(panel => ({ ...panel, windowId: panel.windowId >= 0 ? panel.windowId : windows.length === 1 ? windows[0].id : -1 }));
58
+ };
59
+ })();`,
60
+ });
61
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 });
62
+ const result = await session.send('Runtime.evaluate', {
63
+ expression: `globalThis[Symbol.for(${JSON.stringify(key)})](${JSON.stringify(windowIds)})`,
64
+ userGesture: windowIds.length > 0,
65
+ awaitPromise: true,
66
+ returnByValue: true,
67
+ });
68
+ if (result.exceptionDetails) throw new Error(`Extension sidepanel restoration failed: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}. Next: mm-harness launch --adapter extension --display-mode fullscreen`);
69
+ return result.result.value;
70
+ } finally {
71
+ if (!page.isClosed()) {
72
+ if (preload) await session.send('Page.removeScriptToEvaluateOnNewDocument', { identifier: preload.identifier });
73
+ await session.send('Runtime.evaluate', { expression: `delete globalThis[Symbol.for(${JSON.stringify(key)})]` });
74
+ await session.detach();
75
+ await page.close();
76
+ }
77
+ }
78
+ }
79
+
80
+ async function handoffRuntimeBuild({ target, runtimeDist, dist, cdpPort }) {
81
+ const runtimeDir = path.dirname(runtimeDist);
82
+ const release = acquireBuildHandoff(runtimeDir);
83
+ let browser;
84
+ let manager;
85
+ try {
86
+ if (dist && fs.existsSync(path.join(runtimeDir, 'extension-release-artifact.json'))) {
87
+ throw new Error('Development builds cannot replace a release artifact runtime. Next: launch the development runtime explicitly.');
88
+ }
89
+ const identityPath = path.join(runtimeDir, RUNTIME_IDENTITY_FILENAME);
90
+ // A watcher must not publish while a full launch has stopped the browser.
91
+ if (!fs.existsSync(identityPath)) {
92
+ if (cdpPort) throw new Error('Extension runtime ownership is missing. Next: mm-harness launch --adapter extension');
93
+ return false;
94
+ }
95
+ const identity = JSON.parse(fs.readFileSync(identityPath, 'utf8'));
96
+ if (cdpPort && Number(cdpPort) !== identity.port) throw new Error('Extension CDP port differs from its runtime owner.');
97
+ browser = await connectBrowserViaCdp(`http://127.0.0.1:${identity.port}`);
98
+ const session = await browser.newBrowserCDPSession();
99
+ const { arguments: command } = await session.send('Browser.getBrowserCommandLine');
100
+ if (!command.includes(`${RUNTIME_NONCE_PREFIX}${identity.nonce}`) || !command.includes(`--load-extension=${runtimeDist}`)) {
101
+ throw new Error('Extension build handoff refused a foreign browser.');
102
+ }
103
+ const context = browser.contexts()[0];
104
+ manager = await context.newPage();
105
+ await manager.goto('chrome://extensions/', { waitUntil: 'domcontentloaded', timeout: 15_000 });
106
+ const extensions = await evaluatePageViaCdp(manager, () => new Promise((resolve, reject) => {
107
+ const timer = setTimeout(() => reject(new Error('Extension management timed out.')), 15_000);
108
+ chrome.developerPrivate.getExtensionsInfo({ includeDisabled: true, includeTerminated: true }, (entries) => {
109
+ clearTimeout(timer);
110
+ if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
111
+ else resolve(entries.map(({ id, path: directory }) => ({ id, directory })));
112
+ });
113
+ }));
114
+ const extensionId = extensions.find((entry) => entry.directory === fs.realpathSync(runtimeDist))?.id;
115
+ if (!extensionId) throw new Error('Chrome has not registered the requested unpacked extension directory.');
116
+ const prefix = `chrome-extension://${extensionId}/`;
117
+ const pages = context.pages().filter((page) => page.url().startsWith(prefix)).map((page) => ({ page, url: page.url() }));
118
+ const oldManifest = JSON.parse(fs.readFileSync(path.join(runtimeDist, 'manifest.json'), 'utf8'));
119
+ const panelUrl = oldManifest.side_panel?.default_path && new URL(oldManifest.side_panel.default_path, prefix).href;
120
+ const panels = panelUrl && pages.some((entry) => new URL(entry.url).pathname === new URL(panelUrl).pathname)
121
+ ? await sidePanelContexts(context, panelUrl)
122
+ : [];
123
+ const panelWindowIds = [...new Set(panels.map((panel) => panel.windowId))];
124
+ if (panelWindowIds.some((id) => !Number.isInteger(id) || id < 0)) {
125
+ throw new Error('Extension sidepanel window is ambiguous. Next: close extra windows in this profile before reloading.');
126
+ }
127
+ const oldTargets = (await session.send('Target.getTargets')).targetInfos.filter((entry) => entry.url.startsWith(prefix));
128
+ async function setEnabled(enabled) {
129
+ await evaluatePageViaCdp(manager, ({ id, enabled: next }) => new Promise((resolve, reject) => {
130
+ const timer = setTimeout(() => reject(new Error('Extension enable/disable timed out.')), 15_000);
131
+ chrome.management.setEnabled(id, next, () => {
132
+ clearTimeout(timer);
133
+ if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
134
+ else resolve(true);
135
+ });
136
+ }), { id: extensionId, enabled });
137
+ }
138
+ await setEnabled(false);
139
+ if (dist) {
140
+ const copied = spawnSync('rsync', ['-a', '--delete', '--exclude', '_metadata', `${dist}/`, `${runtimeDist}/`], { encoding: 'utf8' });
141
+ if (copied.status !== 0) throw new Error(`Extension build copy failed: ${copied.stderr || copied.stdout}`);
142
+ const configured = spawnSync(process.execPath, [path.join(__dirname, 'configure-runtime-manifest.cjs'), '--target', target, '--manifest', path.join(runtimeDist, 'manifest.json')], { encoding: 'utf8' });
143
+ if (configured.status !== 0) throw new Error(`Extension runtime configuration failed: ${configured.stderr || configured.stdout}`);
144
+ stampRuntimeTitles({ target, runtimeDist });
145
+ }
146
+ await setEnabled(true);
147
+ for (const entry of pages) {
148
+ if (entry.page.isClosed() && panels.length && new URL(entry.url).pathname === new URL(panelUrl).pathname) continue;
149
+ const page = entry.page.isClosed() ? await context.newPage() : entry.page;
150
+ await page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 15_000 });
151
+ }
152
+ const manifest = JSON.parse(fs.readFileSync(path.join(runtimeDist, 'manifest.json'), 'utf8'));
153
+ if (panels.length) {
154
+ if (!manifest.side_panel?.default_path) {
155
+ throw new Error('Extension sidepanel has no restorable window. Next: mm-harness launch --adapter extension --display-mode fullscreen');
156
+ }
157
+ await sidePanelContexts(context, new URL(manifest.side_panel.default_path, prefix).href, panelWindowIds);
158
+ }
159
+ if (manifest.background?.service_worker) {
160
+ const expected = `${prefix}${manifest.background.service_worker}`;
161
+ const deadline = Date.now() + 15_000;
162
+ let replaced = false;
163
+ while (Date.now() < deadline) {
164
+ const { targetInfos } = await session.send('Target.getTargets');
165
+ replaced = targetInfos.some((entry) => entry.type === 'service_worker' && entry.url === expected && !oldTargets.some((old) => old.targetId === entry.targetId));
166
+ if (replaced) break;
167
+ await new Promise((resolve) => setTimeout(resolve, 100));
168
+ }
169
+ if (!replaced) throw new Error('Extension build handoff did not start a new background worker. Next: mm-harness launch --adapter extension');
170
+ }
171
+ return true;
172
+ } finally {
173
+ try {
174
+ if (manager) await manager.close().catch(() => {});
175
+ if (browser) await browser.close();
176
+ } finally {
177
+ release();
178
+ }
179
+ }
180
+ }
181
+
182
+ module.exports = { acquireBuildHandoff, handoffRuntimeBuild };
183
+
184
+ if (require.main === module) {
185
+ const args = {};
186
+ for (let index = 2; index < process.argv.length; index += 2) {
187
+ const flag = process.argv[index];
188
+ if (flag === '--help') {
189
+ console.log('Usage: build-handoff.cjs --target <checkout> --runtime-dist <directory> [--dist <build>] [--cdp-port <port>]');
190
+ process.exit(0);
191
+ }
192
+ if (!flag.startsWith('--') || !process.argv[index + 1]) throw new Error(`Missing value for ${flag}`);
193
+ args[flag.slice(2)] = process.argv[index + 1];
194
+ }
195
+ if (!args.target || !args['runtime-dist']) throw new Error('build-handoff requires --target and --runtime-dist.');
196
+ handoffRuntimeBuild({ target: path.resolve(args.target), runtimeDist: path.resolve(args['runtime-dist']), dist: args.dist && path.resolve(args.dist), cdpPort: args['cdp-port'] })
197
+ .catch((error) => { console.error(`build-handoff: ${error.message}`); process.exitCode = 1; });
198
+ }
@@ -94,6 +94,7 @@ fs.rmSync(path.join(harnessDir, 'runner/library.json'), { force: true });
94
94
  // The installed overlay has a flat scripts/ runtime API independent of the
95
95
  // package's source layout.
96
96
  copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-browser.cjs'));
97
+ copyFile(path.join(runnerDir, 'adapters/extension/build-handoff.cjs'), path.join(harnessDir, 'scripts/build-handoff.cjs'));
97
98
  copyFile(path.join(runnerDir, 'adapters/extension/console-tail.mjs'), path.join(harnessDir, 'scripts/console-tail.mjs'));
98
99
  copyFile(path.join(runnerDir, 'adapters/extension/launch.sh'), path.join(harnessDir, 'scripts/launch.sh'));
99
100
  copyFile(path.join(runnerDir, 'adapters/extension/live.sh'), path.join(harnessDir, 'scripts/live.sh'));
@@ -25,6 +25,7 @@ const path = require('node:path');
25
25
  const { execFileSync, spawn, spawnSync } = require('node:child_process');
26
26
  const { extensionIdFromExtensionDir } = require('./lib/extension-id.cjs');
27
27
  const { profileProcessPids } = require('./lib/validation-process-ownership.cjs');
28
+ const { acquireBuildHandoff } = require('./build-handoff.cjs');
28
29
  const {
29
30
  automationRuntimeArgs,
30
31
  clearDetachedLaunchUnproven,
@@ -78,6 +79,8 @@ if (args['reset-profile'] !== undefined) assertResettableProfile(args.profile, r
78
79
  fs.mkdirSync(args.profile, { recursive: true });
79
80
  fs.mkdirSync(path.dirname(args['chrome-log']), { recursive: true });
80
81
  fs.mkdirSync(path.dirname(args['chrome-pid']), { recursive: true });
82
+ const releaseBuildHandoff = acquireBuildHandoff(runtimeDir);
83
+ process.on('exit', releaseBuildHandoff);
81
84
 
82
85
  // Ownership guard: only ever take over a CDP endpoint we provably launched. A pid
83
86
  // on this port is ours iff its command line loads our --user-data-dir; anything
@@ -228,6 +228,7 @@ NODE
228
228
  quoted_check_infura="$(printf '%q' "$SCRIPT_DIR/check-infura-readiness.cjs")"
229
229
  quoted_stamp_title="$(printf '%q' "$SCRIPT_DIR/stamp-runtime-title.cjs")"
230
230
  quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
231
+ quoted_build_handoff="$(printf '%q' "$SCRIPT_DIR/build-handoff.cjs")"
231
232
  quoted_artifact_state="$(printf '%q' "$SCRIPT_DIR/artifact-runtime-state.cjs")"
232
233
  quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
233
234
  quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
@@ -301,6 +302,9 @@ NODE
301
302
  quoted_chrome_log="$(printf '%q' "$ARTIFACTS/logs/chrome.log")"
302
303
  quoted_chrome_pid="$(printf '%q' "$ARTIFACTS/logs/chrome.pid")"
303
304
  prepare_parts=()
305
+ reset_profile_arg=""
306
+ $RESET_PROFILE && reset_profile_arg=" --reset-profile 1"
307
+ prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1${reset_profile_arg} --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}")
304
308
  if $EXTERNAL_ARTIFACT; then
305
309
  quoted_artifact_provenance="$(printf '%q' "$ARTIFACT_PROVENANCE")"
306
310
  else
@@ -326,9 +330,6 @@ NODE
326
330
  if ! $EXTERNAL_ARTIFACT; then
327
331
  prepare_parts+=("node ${quoted_configure_manifest} --target ${quoted_target} --manifest ${quoted_runtime_dist}/manifest.json")
328
332
  fi
329
- reset_profile_arg=""
330
- $RESET_PROFILE && reset_profile_arg=" --reset-profile 1"
331
- prepare_parts+=("node ${quoted_chrome_launcher} --stop-only 1${reset_profile_arg} --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}")
332
333
  if ! $EXTERNAL_ARTIFACT; then
333
334
  prepare_parts+=("node ${quoted_stamp_title} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --runtime-dir ${quoted_runtime_dir}")
334
335
  fi
@@ -352,6 +353,7 @@ NODE
352
353
  fi
353
354
  prepare_parts+=("$chrome_launch_cmd")
354
355
  prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
356
+ prepare_parts+=("node ${quoted_build_handoff} --target ${quoted_target} --runtime-dist ${quoted_runtime_dist} --cdp-port ${CDP_PORT}")
355
357
  if [ -n "$WALLET_FIXTURE_ABS" ]; then
356
358
  prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
357
359
  fi
@@ -93,18 +93,13 @@ else
93
93
  RUNTIME_DIR="${RECIPE_RUNTIME_DIR:-temp/recipe/runtime}"
94
94
  fi
95
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.
96
+ # Disable the extension before replacing its build so cached workers cannot
97
+ # request assets from a different compilation.
101
98
  RUNTIME_DIST_ABS="$TARGET/$RUNTIME_DIR/$RUNTIME_DIST_DIR"
102
99
  echo "[reattach] reusing loaded runtime-dist: $RUNTIME_DIST_ABS" >&2
103
100
  if [ -d "$DIST_ABS" ] && [ -d "$RUNTIME_DIST_ABS" ]; then
104
101
  echo "[reattach] refreshing loaded runtime-dist in place" >&2
105
- rsync -a --exclude _metadata "$DIST_ABS/" "$RUNTIME_DIST_ABS/" >&2
106
- node "$SCRIPT_DIR/configure-runtime-manifest.cjs" --target "$TARGET" --manifest "$RUNTIME_DIST_ABS/manifest.json" >&2
107
- node "$SCRIPT_DIR/stamp-runtime-title.cjs" --target "$TARGET" --runtime-dist "$RUNTIME_DIST_ABS" --runtime-dir "$RUNTIME_DIR" >&2
102
+ (cd "$TARGET" && node "$SCRIPT_DIR/build-handoff.cjs" --target "$TARGET" --dist "$DIST_ABS" --runtime-dist "$RUNTIME_DIST_ABS" --cdp-port "$CDP_PORT") >&2
108
103
  fi
109
104
 
110
105
  # Resolve the extension id: explicit flag, then the recorded id file, else CDP.
@@ -225,11 +220,8 @@ async function closePages(context, predicate) {
225
220
  }
226
221
 
227
222
  (async () => {
228
- // Phase 1: attach to the already-loaded extension page. Do not call
229
- // chrome.runtime.reload() here: on the slot Chrome version it can leave the
230
- // target URL as chrome-extension://.../home.html while the actual document is
231
- // chrome-error://chromewebdata. Clean relaunch owns loading a new unpacked
232
- // runtime-dist; quick reattach only refreshes/foregrounds the live UI.
223
+ // The build handoff has already replaced the background worker. Reattach only
224
+ // arranges the requested wallet and dapp pages.
233
225
  let { browser, context } = await connect();
234
226
  const providedExtId = extensionIdFromRuntimeDist() || validExtensionId(process.env.EXT_ID);
235
227
  const extId = extensionIdFrom(context, providedExtId);
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const { spawnSync } = require('node:child_process');
5
4
  const fs = require('node:fs');
6
5
  const path = require('node:path');
7
- const { stampRuntimeTitles } = require('./stamp-runtime-title.cjs');
6
+ const { handoffRuntimeBuild } = require('./build-handoff.cjs');
8
7
 
9
8
  const args = {};
10
9
  for (let index = 2; index < process.argv.length; index += 1) {
@@ -49,37 +48,16 @@ function removeOwnPidFile() {
49
48
  }
50
49
  }
51
50
 
52
- function syncRuntimeDist() {
51
+ async function syncRuntimeDist() {
53
52
  if (!fs.existsSync(path.join(dist, 'manifest.json')) || !fs.existsSync(runtimeDist)) return;
54
- const result = spawnSync('rsync', ['-a', '--delete', '--delay-updates', '--exclude', '_metadata', `${dist}${path.sep}`, `${runtimeDist}${path.sep}`], {
55
- encoding: 'utf8',
56
- });
57
- if (result.status !== 0) {
58
- process.stderr.write(`runtime-dist sync failed: ${(result.stderr || result.stdout || `rsync exited ${result.status}`).trim()}\n`);
59
- return;
53
+ if (await handoffRuntimeBuild({ target: path.resolve(args.target), dist, runtimeDist })) {
54
+ process.stdout.write(`runtime-dist updated with a fresh background worker (${new Date().toISOString()})\n`);
55
+ } else {
56
+ process.stdout.write('build ready; waiting for an owned browser launch\n');
60
57
  }
61
- const configure = spawnSync(
62
- process.execPath,
63
- [
64
- path.join(__dirname, 'configure-runtime-manifest.cjs'),
65
- '--target',
66
- path.resolve(args.target),
67
- '--manifest',
68
- path.join(runtimeDist, 'manifest.json'),
69
- ],
70
- { encoding: 'utf8' },
71
- );
72
- if (configure.status !== 0) {
73
- process.stderr.write(
74
- `runtime-dist configuration failed: ${(configure.stderr || configure.stdout || `node exited ${configure.status}`).trim()}\n`,
75
- );
76
- return;
77
- }
78
- stampRuntimeTitles({ target: path.resolve(args.target), runtimeDist });
79
- process.stdout.write(`runtime-dist updated after webpack compile (${new Date().toISOString()})\n`);
80
58
  }
81
59
 
82
- function poll() {
60
+ async function poll() {
83
61
  if (!processAlive(watcherPid)) {
84
62
  removeOwnPidFile();
85
63
  process.exit(0);
@@ -100,7 +78,7 @@ function poll() {
100
78
  const text = carry + chunk.subarray(0, bytesRead).toString('utf8');
101
79
  const lines = text.split(/\r?\n/u);
102
80
  carry = lines.pop() || '';
103
- if (lines.some((line) => compilePattern.test(line))) syncRuntimeDist();
81
+ if (lines.some((line) => compilePattern.test(line))) await syncRuntimeDist();
104
82
  }
105
83
  } catch (error) {
106
84
  if (error?.code !== 'ENOENT') process.stderr.write(`runtime-dist sync monitor: ${error.message}\n`);
@@ -266,6 +266,14 @@
266
266
  "inputs": "resolve|prefill|seed-cdp; --target --cdp-port --fixture --state --profile --extension-dir --extension-id-file --out --source-out --fixture-script --summary; env RECIPE_WALLET_FIXTURE",
267
267
  "outputs": "fixture path on stdout (resolve), provenance/state/parity JSON files; exit 0/1/2"
268
268
  },
269
+ {
270
+ "id": "extension/build-handoff",
271
+ "entry": "adapters/extension/build-handoff.cjs",
272
+ "kind": "node",
273
+ "purpose": "Replace builds with the owned extension disabled, then refresh its worker and restore native panels without clearing profile storage.",
274
+ "inputs": "--target --runtime-dist [--dist] [--cdp-port]",
275
+ "outputs": "fresh worker and optional build copy; exit 0 / non-zero"
276
+ },
269
277
  {
270
278
  "id": "extension/launch-browser",
271
279
  "entry": "adapters/extension/launch-browser.cjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.50.0",
3
+ "version": "0.50.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"