@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 +4 -0
- package/adapters/extension/build-handoff.cjs +198 -0
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +3 -0
- package/adapters/extension/live.sh +5 -3
- package/adapters/extension/reattach.sh +5 -13
- package/adapters/extension/sync-webpack-dist.cjs +8 -30
- package/adapters/manifest.json +8 -0
- package/package.json +1 -1
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
|
-
#
|
|
97
|
-
#
|
|
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
|
-
|
|
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
|
-
//
|
|
229
|
-
//
|
|
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 {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
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`);
|
package/adapters/manifest.json
CHANGED
|
@@ -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",
|