@deeeed/metamask-harness 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +161 -0
- package/README.md +140 -0
- package/bin/mm-harness +99 -0
- package/docs/CHEATSHEET.md +61 -0
- package/docs/CLI-SPEC.md +915 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/architecture.md +367 -0
- package/docs/extension-runtime-commands.md +60 -0
- package/docs/harness-cli.md +43 -0
- package/docs/live-adapter-contract.md +188 -0
- package/docs/package-boundaries.md +47 -0
- package/docs/perps-flow-catalog.md +235 -0
- package/docs/recipe-libraries.md +95 -0
- package/docs/runtime-file-conventions.md +36 -0
- package/library/actions/core/perps/_controller.mjs +727 -0
- package/library/actions/core/perps/assert_orders.mjs +53 -0
- package/library/actions/core/perps/assert_positions.mjs +52 -0
- package/library/actions/core/perps/close_orders.mjs +97 -0
- package/library/actions/core/perps/close_positions.mjs +118 -0
- package/library/actions/core/perps/ensure_orders.mjs +40 -0
- package/library/actions/core/perps/ensure_positions.mjs +37 -0
- package/library/actions/core/perps/place_order.mjs +201 -0
- package/library/actions/core/perps/read_account.mjs +30 -0
- package/library/actions/core/perps/read_orders.mjs +27 -0
- package/library/actions/core/perps/read_positions.mjs +27 -0
- package/library/actions/core/perps/start_state.mjs +92 -0
- package/library/actions/core/perps/teardown_state.mjs +86 -0
- package/library/actions/extension/perps/assert_orders.mjs +11 -0
- package/library/actions/extension/perps/assert_positions.mjs +11 -0
- package/library/actions/extension/perps/close_orders.mjs +8 -0
- package/library/actions/extension/perps/close_positions.mjs +8 -0
- package/library/actions/extension/perps/ensure_orders.mjs +4 -0
- package/library/actions/extension/perps/ensure_positions.mjs +4 -0
- package/library/actions/extension/perps/perps.mjs +730 -0
- package/library/actions/extension/perps/place_order.mjs +7 -0
- package/library/actions/extension/perps/read_orders.mjs +4 -0
- package/library/actions/extension/perps/read_positions.mjs +3 -0
- package/library/actions/extension/platform/cdp.mjs +541 -0
- package/library/actions/extension/ui/navigate.mjs +44 -0
- package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
- package/library/actions/extension/wallet/read_state.mjs +27 -0
- package/library/actions/extension/wallet/select_account.mjs +48 -0
- package/library/actions/extension/wallet/setup.mjs +35 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
- package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
- package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
- package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
- package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
- package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
- package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
- package/library/actions/mobile/perps/assert_orders.mjs +11 -0
- package/library/actions/mobile/perps/assert_positions.mjs +11 -0
- package/library/actions/mobile/perps/close_orders.mjs +8 -0
- package/library/actions/mobile/perps/close_positions.mjs +8 -0
- package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
- package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
- package/library/actions/mobile/perps/perps.mjs +709 -0
- package/library/actions/mobile/perps/place_order.mjs +7 -0
- package/library/actions/mobile/perps/read_orders.mjs +4 -0
- package/library/actions/mobile/perps/read_positions.mjs +3 -0
- package/library/actions/mobile/platform/bridge.mjs +283 -0
- package/library/actions/mobile/ui/navigate.mjs +38 -0
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
- package/library/actions/mobile/wallet/home.mjs +35 -0
- package/library/actions/mobile/wallet/read_state.mjs +40 -0
- package/library/actions/mobile/wallet/select_account.mjs +48 -0
- package/library/actions/mobile/wallet/setup.mjs +220 -0
- package/library/flows/perps.flows.json +64 -0
- package/library/library.json +7 -0
- package/library/manifests/core.action-manifest.json +1282 -0
- package/library/manifests/extension.action-manifest.json +1749 -0
- package/library/manifests/mobile.action-manifest.json +1753 -0
- package/library/recipes/action-validation.extension.recipe.json +417 -0
- package/library/recipes/action-validation.mobile.recipe.json +422 -0
- package/library/recipes/order-lifecycle.core.recipe.json +78 -0
- package/library/recipes/perps-lifecycle.recipe.json +194 -0
- package/library/recipes/read-markets.core.recipe.json +38 -0
- package/library/recipes/smoke.extension.recipe.json +31 -0
- package/library/recipes/smoke.mobile.recipe.json +31 -0
- package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
- package/orchestration/compat-overlays/README.md +19 -0
- package/orchestration/compat-overlays/mobile/README.md +13 -0
- package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
- package/orchestration/core/cleanup.sh +37 -0
- package/orchestration/core/inject.sh +154 -0
- package/orchestration/doctor.mjs +72 -0
- package/orchestration/extension/cleanup.mjs +60 -0
- package/orchestration/extension/console-tail.mjs +228 -0
- package/orchestration/extension/ensure-browser.sh +416 -0
- package/orchestration/extension/ensure-ready.ts +185 -0
- package/orchestration/extension/extension-id.ts +107 -0
- package/orchestration/extension/inject.mjs +266 -0
- package/orchestration/extension/launch-browser.cjs +216 -0
- package/orchestration/extension/launch.sh +175 -0
- package/orchestration/extension/live.sh +320 -0
- package/orchestration/extension/pin-remote-flags.cjs +45 -0
- package/orchestration/extension/readiness.mjs +414 -0
- package/orchestration/extension/refresh-build.sh +190 -0
- package/orchestration/extension/runtime-decision.ts +445 -0
- package/orchestration/extension/runtime.ts +407 -0
- package/orchestration/extension/seed-fixture.sh +177 -0
- package/orchestration/extension/sidepanel-toggle.sh +291 -0
- package/orchestration/extension/snapshot-dist.sh +84 -0
- package/orchestration/extension/start-watch.sh +339 -0
- package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
- package/orchestration/lib/activate-repo-node.sh +144 -0
- package/orchestration/lib/cli-color.mjs +84 -0
- package/orchestration/lib/cli-commands.mjs +243 -0
- package/orchestration/lib/cli-home.mjs +354 -0
- package/orchestration/lib/cli-ux.sh +252 -0
- package/orchestration/lib/cli-version.mjs +123 -0
- package/orchestration/lib/ensure-runner-deps.sh +56 -0
- package/orchestration/lib/harness-path.sh +55 -0
- package/orchestration/lib/hash-helpers.sh +44 -0
- package/orchestration/lib/json-field.sh +23 -0
- package/orchestration/lib/log-tui.mjs +304 -0
- package/orchestration/lib/open-debug.mjs +317 -0
- package/orchestration/lib/path-defaults.json +4 -0
- package/orchestration/lib/progress.mjs +107 -0
- package/orchestration/lib/recipe-paths.mjs +26 -0
- package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
- package/orchestration/manifest.json +358 -0
- package/orchestration/mobile/cleanup.sh +192 -0
- package/orchestration/mobile/deps-markers.ts +21 -0
- package/orchestration/mobile/inject.sh +681 -0
- package/orchestration/mobile/launch.sh +137 -0
- package/orchestration/mobile/live.sh +125 -0
- package/orchestration/mobile/runtime-decision.ts +292 -0
- package/orchestration/porcelain/metamask-recipe +99 -0
- package/orchestration/porcelain/mm-recipe +1591 -0
- package/orchestration/porcelain/mme-recipe +1181 -0
- package/package.json +59 -0
- package/runner/extension/verify.sh +511 -0
- package/runner/mobile/verify.sh +501 -0
- package/runner/src/adapters.ts +601 -0
- package/runner/src/cli.ts +1820 -0
- package/runner/src/commands/debug.ts +44 -0
- package/runner/src/commands/fixtures.ts +99 -0
- package/runner/src/commands/launch.ts +397 -0
- package/runner/src/commands/logs.ts +60 -0
- package/runner/src/commands/shared.ts +138 -0
- package/runner/src/completions-cache.ts +86 -0
- package/runner/src/doctor.ts +203 -0
- package/runner/src/harness.ts +516 -0
- package/runner/src/heal-bounds.ts +179 -0
- package/runner/src/index.ts +6 -0
- package/runner/src/live-adapter-contract.ts +274 -0
- package/runner/src/manifest.ts +47 -0
- package/runner/src/mm-harness-cli.ts +488 -0
- package/runner/src/paths.ts +198 -0
- package/runner/src/recording-target.ts +147 -0
- package/runner/src/run-recording.ts +329 -0
- package/runner/src/runner.ts +108 -0
- package/runner/src/types.ts +57 -0
- package/scripts/completions.sh +125 -0
- package/scripts/install-completions.sh +62 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// readiness.mjs — health probe for one extension instance
|
|
3
|
+
// runner/extension/extension-readiness.mjs)
|
|
4
|
+
//
|
|
5
|
+
// Purpose:
|
|
6
|
+
// App-control health probe: verifies the dist build's expected entry
|
|
7
|
+
// files and (with --cdp-port) the live extension target over CDP,
|
|
8
|
+
// repairing <runtime-dir>/extension.id when the runtime disagrees.
|
|
9
|
+
//
|
|
10
|
+
// Inputs (flags / env):
|
|
11
|
+
// --target <metamask-extension> (default $PWD), --cdp-port <port>, --json
|
|
12
|
+
// env RECIPE_RUNTIME_DIR (else lib path-defaults)
|
|
13
|
+
// Outputs: readiness report (JSON with --json) on stdout.
|
|
14
|
+
// Exit 0 — ready; non-zero — not ready / bad args (thrown message).
|
|
15
|
+
// Never touches: the dist build (read-only) other than the
|
|
16
|
+
// <runtime-dir>/extension.id repair.
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import http from 'node:http';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
function pathDefault(key) {
|
|
27
|
+
for (const candidate of [
|
|
28
|
+
path.join(scriptDir, 'lib/path-defaults.json'),
|
|
29
|
+
path.join(scriptDir, '../lib/path-defaults.json'),
|
|
30
|
+
path.join(scriptDir, '../../orchestration/lib/path-defaults.json'),
|
|
31
|
+
]) {
|
|
32
|
+
if (!fs.existsSync(candidate)) continue;
|
|
33
|
+
const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
|
|
34
|
+
if (value) return value;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Missing path default: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function recipeRuntimeDir() {
|
|
40
|
+
return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const out = { target: process.cwd(), cdpPort: '', json: false };
|
|
45
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
46
|
+
const arg = argv[i];
|
|
47
|
+
if (arg === '--target') {
|
|
48
|
+
out.target = argv[++i];
|
|
49
|
+
} else if (arg === '--cdp-port') {
|
|
50
|
+
out.cdpPort = argv[++i];
|
|
51
|
+
} else if (arg === '--json') {
|
|
52
|
+
out.json = true;
|
|
53
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
54
|
+
console.log('Usage: readiness.mjs --target <metamask-extension> [--cdp-port <port>] [--json]');
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`Unknown arg: ${arg}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readManifest(target) {
|
|
64
|
+
const manifestPath = path.join(target, 'dist/chrome/manifest.json');
|
|
65
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
66
|
+
return { manifestPath, manifest };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function manifestExpectedFiles(manifest) {
|
|
70
|
+
const entries = new Set(['home.html']);
|
|
71
|
+
if (manifest.background?.service_worker) entries.add(manifest.background.service_worker);
|
|
72
|
+
for (const script of manifest.background?.scripts || []) entries.add(script);
|
|
73
|
+
const popup =
|
|
74
|
+
manifest.action?.default_popup ||
|
|
75
|
+
manifest.browser_action?.default_popup ||
|
|
76
|
+
manifest.page_action?.default_popup;
|
|
77
|
+
if (popup) entries.add(popup);
|
|
78
|
+
if (manifest.side_panel?.default_path) entries.add(manifest.side_panel.default_path);
|
|
79
|
+
return [...entries];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function manifestDefaultPage(manifest) {
|
|
83
|
+
return (
|
|
84
|
+
manifest.action?.default_popup ||
|
|
85
|
+
manifest.browser_action?.default_popup ||
|
|
86
|
+
manifest.page_action?.default_popup ||
|
|
87
|
+
manifest.side_panel?.default_path ||
|
|
88
|
+
'home.html'
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extensionIdPath(target) {
|
|
93
|
+
return path.join(target, recipeRuntimeDir(), 'extension.id');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readExpectedExtensionId(target) {
|
|
97
|
+
const idPath = path.join(target, recipeRuntimeDir(), 'extension.id');
|
|
98
|
+
if (!fs.existsSync(idPath)) return '';
|
|
99
|
+
const id = fs.readFileSync(idPath, 'utf8').trim();
|
|
100
|
+
return /^[a-p]{32}$/.test(id) ? id : '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function writeExtensionId(target, extensionId) {
|
|
104
|
+
if (!/^[a-p]{32}$/.test(extensionId)) return false;
|
|
105
|
+
const idPath = extensionIdPath(target);
|
|
106
|
+
fs.mkdirSync(path.dirname(idPath), { recursive: true });
|
|
107
|
+
const existing = fs.existsSync(idPath) ? fs.readFileSync(idPath, 'utf8').trim() : '';
|
|
108
|
+
if (existing === extensionId) return false;
|
|
109
|
+
fs.writeFileSync(idPath, `${extensionId}\n`);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function httpJson(url, timeoutMs = 3000) {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const req = http.get(url, (res) => {
|
|
116
|
+
let data = '';
|
|
117
|
+
res.setEncoding('utf8');
|
|
118
|
+
res.on('data', (chunk) => {
|
|
119
|
+
data += chunk;
|
|
120
|
+
});
|
|
121
|
+
res.on('end', () => {
|
|
122
|
+
try {
|
|
123
|
+
resolve(JSON.parse(data));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
reject(new Error(`invalid JSON from ${url}: ${err.message}`));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
req.setTimeout(timeoutMs, () => {
|
|
130
|
+
req.destroy(new Error(`timeout from ${url}`));
|
|
131
|
+
});
|
|
132
|
+
req.on('error', reject);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function httpJsonRequest(method, url, timeoutMs = 3000) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const req = http.request(url, { method }, (res) => {
|
|
139
|
+
let data = '';
|
|
140
|
+
res.setEncoding('utf8');
|
|
141
|
+
res.on('data', (chunk) => {
|
|
142
|
+
data += chunk;
|
|
143
|
+
});
|
|
144
|
+
res.on('end', () => {
|
|
145
|
+
try {
|
|
146
|
+
resolve(JSON.parse(data));
|
|
147
|
+
} catch (err) {
|
|
148
|
+
reject(new Error(`invalid JSON from ${method} ${url}: ${err.message}`));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
req.setTimeout(timeoutMs, () => {
|
|
153
|
+
req.destroy(new Error(`timeout from ${method} ${url}`));
|
|
154
|
+
});
|
|
155
|
+
req.on('error', reject);
|
|
156
|
+
req.end();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveWebSocket(target) {
|
|
161
|
+
try {
|
|
162
|
+
return require(require.resolve('ws', { paths: [target, process.cwd()] }));
|
|
163
|
+
} catch {
|
|
164
|
+
return typeof WebSocket === 'function' ? WebSocket : null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
|
|
169
|
+
const WebSocketImpl = resolveWebSocket(target);
|
|
170
|
+
if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable in this Node runtime' };
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const ws = new WebSocketImpl(webSocketDebuggerUrl);
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
try {
|
|
175
|
+
ws.close();
|
|
176
|
+
} catch {
|
|
177
|
+
// Best effort timeout cleanup.
|
|
178
|
+
}
|
|
179
|
+
reject(new Error('timeout evaluating extension page via CDP'));
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
const onOpen = () => {
|
|
182
|
+
ws.send(JSON.stringify({
|
|
183
|
+
id: 1,
|
|
184
|
+
method: 'Runtime.evaluate',
|
|
185
|
+
params: { expression, awaitPromise: true, returnByValue: true },
|
|
186
|
+
}));
|
|
187
|
+
};
|
|
188
|
+
const onMessage = (event) => {
|
|
189
|
+
const raw = event?.data ?? event;
|
|
190
|
+
const msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
|
|
191
|
+
if (msg.id !== 1) return;
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
ws.close();
|
|
194
|
+
if (msg.error) {
|
|
195
|
+
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
resolve(msg.result?.result?.value ?? null);
|
|
199
|
+
};
|
|
200
|
+
const onError = (err) => {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
reject(new Error(`CDP websocket error while inspecting extension page: ${err?.message || err || 'unknown'}`));
|
|
203
|
+
};
|
|
204
|
+
if (typeof ws.on === 'function') {
|
|
205
|
+
ws.on('open', onOpen);
|
|
206
|
+
ws.on('message', onMessage);
|
|
207
|
+
ws.on('error', onError);
|
|
208
|
+
} else {
|
|
209
|
+
ws.addEventListener('open', onOpen);
|
|
210
|
+
ws.addEventListener('message', onMessage);
|
|
211
|
+
ws.addEventListener('error', onError);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function extensionIdFromTarget(target) {
|
|
217
|
+
const url = String(target.url || '');
|
|
218
|
+
const match = url.match(/^chrome-extension:\/\/([a-p]{32})\//u);
|
|
219
|
+
return match ? match[1] : '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker) {
|
|
223
|
+
const byId = new Map();
|
|
224
|
+
for (const target of targets) {
|
|
225
|
+
const id = extensionIdFromTarget(target);
|
|
226
|
+
if (!id) continue;
|
|
227
|
+
const entries = byId.get(id) || [];
|
|
228
|
+
entries.push(target);
|
|
229
|
+
byId.set(id, entries);
|
|
230
|
+
}
|
|
231
|
+
const ids = [...byId.keys()];
|
|
232
|
+
if (ids.length === 0) return { extensionIds: [], selectedExtensionId: '' };
|
|
233
|
+
|
|
234
|
+
const hasExpectedWorker = (id) => byId.get(id).some((target) => {
|
|
235
|
+
const url = String(target.url || '');
|
|
236
|
+
return target.type === 'service_worker' && expectedServiceWorker && url.endsWith(`/${expectedServiceWorker}`);
|
|
237
|
+
});
|
|
238
|
+
const hasAnyWorker = (id) => byId.get(id).some((target) => target.type === 'service_worker');
|
|
239
|
+
|
|
240
|
+
const selectedExtensionId =
|
|
241
|
+
ids.find((id) => id === expectedExtensionId && hasExpectedWorker(id)) ||
|
|
242
|
+
ids.find((id) => hasExpectedWorker(id)) ||
|
|
243
|
+
ids.find((id) => id === expectedExtensionId && hasAnyWorker(id)) ||
|
|
244
|
+
ids.find((id) => hasAnyWorker(id)) ||
|
|
245
|
+
(expectedExtensionId && ids.includes(expectedExtensionId) ? expectedExtensionId : ids[0]);
|
|
246
|
+
|
|
247
|
+
return { extensionIds: ids, selectedExtensionId };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function openExtensionPage(cdpPort, extensionId, pagePath) {
|
|
251
|
+
const normalizedPath = String(pagePath || 'home.html').replace(/^\/+/u, '');
|
|
252
|
+
const url = `chrome-extension://${extensionId}/${normalizedPath}`;
|
|
253
|
+
try {
|
|
254
|
+
await httpJsonRequest('PUT', `http://127.0.0.1:${cdpPort}/json/new?${encodeURIComponent(url)}`, 3000);
|
|
255
|
+
return true;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
// Some CDP implementations disable /json/new. Readiness can still pass
|
|
258
|
+
// when the caller already opened an extension page, so report this as a
|
|
259
|
+
// best-effort page-open miss instead of hiding a required check failure.
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findPageTarget(targets, selectedExtensionId, preferredPagePath = '') {
|
|
265
|
+
const extensionPages = targets.filter((target) => {
|
|
266
|
+
const url = String(target.url || '');
|
|
267
|
+
return (
|
|
268
|
+
target.type === 'page' &&
|
|
269
|
+
url.startsWith(`chrome-extension://${selectedExtensionId}/`) &&
|
|
270
|
+
typeof target.webSocketDebuggerUrl === 'string'
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
const normalizedPreferred = String(preferredPagePath || '').replace(/^\/+/u, '');
|
|
274
|
+
return (
|
|
275
|
+
extensionPages.find((target) => normalizedPreferred && String(target.url || '').endsWith(`/${normalizedPreferred}`)) ||
|
|
276
|
+
extensionPages.find((target) => !String(target.url || '').includes('/popup-init.html')) ||
|
|
277
|
+
extensionPages[0]
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceWorker, extensionPagePath) {
|
|
282
|
+
const version = await httpJson(`http://127.0.0.1:${cdpPort}/json/version`);
|
|
283
|
+
let targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
284
|
+
if (!Array.isArray(targets)) throw new Error('/json/list did not return an array');
|
|
285
|
+
let { extensionIds, selectedExtensionId } = chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker);
|
|
286
|
+
if (!selectedExtensionId) {
|
|
287
|
+
throw new Error('CDP is reachable but no chrome-extension:// targets are present');
|
|
288
|
+
}
|
|
289
|
+
let pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
|
|
290
|
+
let openedPage = false;
|
|
291
|
+
if (!pageTarget) {
|
|
292
|
+
openedPage = await openExtensionPage(cdpPort, selectedExtensionId, extensionPagePath);
|
|
293
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
294
|
+
targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
295
|
+
({ extensionIds, selectedExtensionId } = chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker));
|
|
296
|
+
pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
|
|
297
|
+
}
|
|
298
|
+
let ui = null;
|
|
299
|
+
if (pageTarget) {
|
|
300
|
+
ui = await cdpEvaluate(
|
|
301
|
+
target,
|
|
302
|
+
pageTarget.webSocketDebuggerUrl,
|
|
303
|
+
`(() => {
|
|
304
|
+
const text = document.body?.innerText || '';
|
|
305
|
+
const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
|
|
306
|
+
// React error-boundary screen (ui/pages/error-page); deterministic testids.
|
|
307
|
+
const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
|
|
308
|
+
const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
|
|
309
|
+
// Testid-only trigger: ui/pages/error-page reliably renders these testids.
|
|
310
|
+
// Avoid matching the visible phrase in body text (false positives).
|
|
311
|
+
const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
|
|
312
|
+
return {
|
|
313
|
+
title: document.title,
|
|
314
|
+
url: location.href,
|
|
315
|
+
textSample: text.slice(0, 500),
|
|
316
|
+
hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
|
|
317
|
+
hasErrorBoundary,
|
|
318
|
+
errorBoundaryName,
|
|
319
|
+
errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
|
|
320
|
+
};
|
|
321
|
+
})()`,
|
|
322
|
+
);
|
|
323
|
+
if (ui && !ui.skipped && ui.hasStartupError) {
|
|
324
|
+
throw Object.assign(new Error('MetaMask extension page loaded startup error UI'), {
|
|
325
|
+
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (ui && !ui.skipped && ui.hasErrorBoundary) {
|
|
329
|
+
const detail = [ui.errorBoundaryName, ui.errorBoundaryMessage].filter(Boolean).join(': ');
|
|
330
|
+
throw Object.assign(new Error(`MetaMask UI crashed (React error boundary)${detail ? `: ${detail}` : ''}`), {
|
|
331
|
+
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (ui && !ui.skipped && String(ui.url || '').startsWith('chrome-error://')) {
|
|
335
|
+
throw Object.assign(new Error('MetaMask extension page loaded Chrome error UI'), {
|
|
336
|
+
report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
browser: version.Browser || 'unknown',
|
|
342
|
+
extensionIds,
|
|
343
|
+
selectedExtensionId,
|
|
344
|
+
markerMatched: Boolean(expectedExtensionId && selectedExtensionId === expectedExtensionId),
|
|
345
|
+
targetCount: targets.length,
|
|
346
|
+
openedPage,
|
|
347
|
+
openedPagePath: extensionPagePath,
|
|
348
|
+
ui,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function main() {
|
|
353
|
+
const args = parseArgs(process.argv.slice(2));
|
|
354
|
+
const target = path.resolve(args.target);
|
|
355
|
+
const checks = [];
|
|
356
|
+
const { manifestPath, manifest } = readManifest(target);
|
|
357
|
+
const expectedFiles = manifestExpectedFiles(manifest);
|
|
358
|
+
const missingFiles = [];
|
|
359
|
+
for (const rel of expectedFiles) {
|
|
360
|
+
const exists = fs.existsSync(path.join(target, 'dist/chrome', rel));
|
|
361
|
+
checks.push({ name: `dist/chrome/${rel}`, status: exists ? 'pass' : 'fail' });
|
|
362
|
+
if (!exists) missingFiles.push(rel);
|
|
363
|
+
}
|
|
364
|
+
if (missingFiles.length > 0) {
|
|
365
|
+
throw Object.assign(
|
|
366
|
+
new Error(`extension build incomplete; missing ${missingFiles.join(', ')}`),
|
|
367
|
+
{ report: { target, manifestPath, expectedFiles, checks } },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const defaultPage = fs.existsSync(path.join(target, 'dist/chrome/home.html'))
|
|
372
|
+
? 'home.html'
|
|
373
|
+
: manifestDefaultPage(manifest);
|
|
374
|
+
const report = {
|
|
375
|
+
target,
|
|
376
|
+
manifestPath,
|
|
377
|
+
manifestVersion: manifest.manifest_version || null,
|
|
378
|
+
defaultPage,
|
|
379
|
+
expectedFiles,
|
|
380
|
+
checks,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (args.cdpPort) {
|
|
384
|
+
const expectedExtensionId = readExpectedExtensionId(target);
|
|
385
|
+
report.cdp = await inspectCdp(
|
|
386
|
+
target,
|
|
387
|
+
args.cdpPort,
|
|
388
|
+
expectedExtensionId,
|
|
389
|
+
manifest.background?.service_worker || '',
|
|
390
|
+
report.defaultPage,
|
|
391
|
+
);
|
|
392
|
+
report.cdp.markerRepaired = writeExtensionId(target, report.cdp.selectedExtensionId);
|
|
393
|
+
checks.push({ name: `CDP ${args.cdpPort} extension targets`, status: 'pass' });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (args.json) {
|
|
397
|
+
console.log(JSON.stringify(report, null, 2));
|
|
398
|
+
} else {
|
|
399
|
+
console.log(`Extension readiness OK: ${expectedFiles.join(', ')}`);
|
|
400
|
+
if (report.cdp) {
|
|
401
|
+
console.log(`CDP OK: ${report.cdp.browser}; extensions=${report.cdp.extensionIds.join(',')}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
main().catch((err) => {
|
|
407
|
+
const report = err && err.report ? err.report : null;
|
|
408
|
+
if (process.argv.includes('--json')) {
|
|
409
|
+
console.log(JSON.stringify({ status: 'fail', error: err.message, ...(report || {}) }, null, 2));
|
|
410
|
+
} else {
|
|
411
|
+
console.error(`Extension readiness failed: ${err.message}`);
|
|
412
|
+
}
|
|
413
|
+
process.exit(1);
|
|
414
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# refresh-build.sh — one-shot rebuild for a frozen extension slot
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# bash refresh-build.sh --repo /path/to/metamask-extension [--watcher-port 9013]
|
|
6
|
+
#
|
|
7
|
+
# Context:
|
|
8
|
+
# In sidepanel mode, preflight.sh defaults to `watch=off` and kills the
|
|
9
|
+
# webpack watcher once dist/chrome is stable. That guarantees side-panel
|
|
10
|
+
# close/reopen never races a rebuild, but means code edits do not reach
|
|
11
|
+
# the browser until an explicit rebuild.
|
|
12
|
+
#
|
|
13
|
+
# This script runs `yarn start` once, waits until the critical extension
|
|
14
|
+
# entry points are back on disk, then kills the watcher again — leaving
|
|
15
|
+
# dist/chrome fresh AND frozen. After it returns:
|
|
16
|
+
# - Close any open extension pages (sidepanel / popup / fullscreen)
|
|
17
|
+
# - Reopen them to pick up the new build
|
|
18
|
+
# e.g. `bash sidepanel-toggle.sh cycle --cdp-port $CDP_PORT`
|
|
19
|
+
#
|
|
20
|
+
# Exit codes:
|
|
21
|
+
# 0 — success
|
|
22
|
+
# 1 — bad args / repo missing
|
|
23
|
+
# 2 — rebuild did not complete within the timeout
|
|
24
|
+
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
REPO=""
|
|
28
|
+
WATCHER_PORT=""
|
|
29
|
+
TIMEOUT_S="180"
|
|
30
|
+
CLEAN_TIMEOUT_S=""
|
|
31
|
+
while [[ $# -gt 0 ]]; do
|
|
32
|
+
case "$1" in
|
|
33
|
+
--repo) REPO="$2"; shift 2 ;;
|
|
34
|
+
--watcher-port) WATCHER_PORT="$2"; shift 2 ;;
|
|
35
|
+
--timeout) TIMEOUT_S="$2"; shift 2 ;;
|
|
36
|
+
--clean-timeout) CLEAN_TIMEOUT_S="$2"; shift 2 ;;
|
|
37
|
+
-h|--help)
|
|
38
|
+
echo "Usage: refresh-build.sh --repo /path/to/metamask-extension [--watcher-port <port>] [--timeout <s>] [--clean-timeout <s>]"
|
|
39
|
+
exit 0
|
|
40
|
+
;;
|
|
41
|
+
*) echo "unknown arg: $1" >&2; exit 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [ -z "$REPO" ] || [ ! -f "$REPO/package.json" ]; then
|
|
46
|
+
echo "FAIL: --repo must point to a metamask-extension checkout" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
PORT_ARG=""
|
|
51
|
+
if [ -n "$WATCHER_PORT" ]; then
|
|
52
|
+
PORT_ARG="PORT=${WATCHER_PORT}"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
cd "$REPO"
|
|
56
|
+
|
|
57
|
+
HTML_ENTRIES=()
|
|
58
|
+
while IFS= read -r entry; do
|
|
59
|
+
[ -n "$entry" ] && HTML_ENTRIES+=("dist/chrome/$entry")
|
|
60
|
+
done < <(node <<'NODE'
|
|
61
|
+
const fs = require('fs');
|
|
62
|
+
const manifestPath = 'dist/chrome/manifest.json';
|
|
63
|
+
if (!fs.existsSync(manifestPath)) {
|
|
64
|
+
console.log('home.html');
|
|
65
|
+
console.log('scripts/app-init.js');
|
|
66
|
+
console.log('popup-init.html');
|
|
67
|
+
console.log('sidepanel.html');
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
71
|
+
const entries = new Set(['home.html']);
|
|
72
|
+
if (manifest.background?.service_worker) entries.add(manifest.background.service_worker);
|
|
73
|
+
for (const script of manifest.background?.scripts || []) entries.add(script);
|
|
74
|
+
const popup = manifest.action?.default_popup || manifest.browser_action?.default_popup || manifest.page_action?.default_popup;
|
|
75
|
+
if (popup) entries.add(popup);
|
|
76
|
+
if (manifest.side_panel?.default_path) entries.add(manifest.side_panel.default_path);
|
|
77
|
+
for (const entry of entries) console.log(entry);
|
|
78
|
+
NODE
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
ASDF_SHIMS="${ASDF_DATA_DIR:-$HOME/.asdf}/shims"
|
|
82
|
+
if [ -d "$ASDF_SHIMS" ]; then
|
|
83
|
+
export PATH="$ASDF_SHIMS:$PATH"
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
87
|
+
# shellcheck disable=SC1091
|
|
88
|
+
for _arn in "$SCRIPT_DIR/lib/activate-repo-node.sh" "$SCRIPT_DIR/../lib/activate-repo-node.sh"; do
|
|
89
|
+
[ -f "$_arn" ] && { . "$_arn"; break; }
|
|
90
|
+
done
|
|
91
|
+
unset _arn
|
|
92
|
+
if command -v require_repo_node >/dev/null 2>&1; then
|
|
93
|
+
require_repo_node "$REPO"
|
|
94
|
+
elif command -v activate_repo_node >/dev/null 2>&1; then
|
|
95
|
+
activate_repo_node "$REPO" || {
|
|
96
|
+
echo "[refresh] refusing to run yarn start without a pinned Node for $REPO" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
echo "[refresh] activate-repo-node.sh missing from harness; run: recipe sync" >&2
|
|
101
|
+
exit 1
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo "[refresh] Using node $(node -v 2>/dev/null || echo 'not found')"
|
|
105
|
+
echo "[refresh] Starting yarn start (watcher-port=${WATCHER_PORT:-default})..."
|
|
106
|
+
LOG="$(mktemp -t refresh-build-XXXXXX.log)"
|
|
107
|
+
echo "[refresh] Log: $LOG"
|
|
108
|
+
env ${PORT_ARG} yarn start > "$LOG" 2>&1 &
|
|
109
|
+
BUILD_PID=$!
|
|
110
|
+
echo "[refresh] Build PID: $BUILD_PID"
|
|
111
|
+
|
|
112
|
+
cleanup() {
|
|
113
|
+
if kill -0 "$BUILD_PID" 2>/dev/null; then
|
|
114
|
+
kill "$BUILD_PID" 2>/dev/null || true
|
|
115
|
+
fi
|
|
116
|
+
# Also clear any child yarn/gulp workers holding the watcher port
|
|
117
|
+
if [ -n "$WATCHER_PORT" ]; then
|
|
118
|
+
lsof -i ":$WATCHER_PORT" -t 2>/dev/null | xargs kill 2>/dev/null || true
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
121
|
+
trap cleanup EXIT
|
|
122
|
+
|
|
123
|
+
# First wait for the clean pass (confirms this build, not a pre-existing stale
|
|
124
|
+
# dist, owns the next entry files we observe).
|
|
125
|
+
CLEAN_TIMEOUT_S="${CLEAN_TIMEOUT_S:-$TIMEOUT_S}"
|
|
126
|
+
echo "[refresh] Waiting for clean pass (timeout ${CLEAN_TIMEOUT_S}s)..."
|
|
127
|
+
for i in $(seq 1 "$CLEAN_TIMEOUT_S"); do
|
|
128
|
+
if grep -q "Finished 'clean'" "$LOG" 2>/dev/null; then
|
|
129
|
+
echo "[refresh] Clean pass reached (${i}s)"
|
|
130
|
+
break
|
|
131
|
+
fi
|
|
132
|
+
if grep -q "webpack.Progress.*100%" "$LOG" 2>/dev/null; then
|
|
133
|
+
echo "[refresh] Webpack reached 100% (${i}s)"
|
|
134
|
+
break
|
|
135
|
+
fi
|
|
136
|
+
if ! kill -0 "$BUILD_PID" 2>/dev/null; then
|
|
137
|
+
echo "FAIL: yarn start exited before clean pass" >&2
|
|
138
|
+
tail -40 "$LOG" 2>/dev/null || true
|
|
139
|
+
exit 2
|
|
140
|
+
fi
|
|
141
|
+
if [ $((i % 10)) -eq 0 ]; then
|
|
142
|
+
echo "[refresh] still waiting for clean... (${i}s)"
|
|
143
|
+
tail -5 "$LOG" 2>/dev/null | sed 's/^/[refresh:log] /' || true
|
|
144
|
+
fi
|
|
145
|
+
sleep 1
|
|
146
|
+
done
|
|
147
|
+
if ! grep -q "Finished 'clean'" "$LOG" 2>/dev/null && ! grep -q "webpack.Progress.*100%" "$LOG" 2>/dev/null; then
|
|
148
|
+
echo "FAIL: build did not reach clean/100% within ${CLEAN_TIMEOUT_S}s" >&2
|
|
149
|
+
tail -40 "$LOG" 2>/dev/null || true
|
|
150
|
+
exit 2
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
echo "[refresh] Waiting for entry points (timeout ${TIMEOUT_S}s): ${HTML_ENTRIES[*]}"
|
|
154
|
+
for i in $(seq 1 "$TIMEOUT_S"); do
|
|
155
|
+
ready=true
|
|
156
|
+
for entry in "${HTML_ENTRIES[@]}"; do
|
|
157
|
+
if [ ! -f "$REPO/${entry}" ]; then
|
|
158
|
+
ready=false
|
|
159
|
+
break
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
$ready && break
|
|
163
|
+
if [ $((i % 15)) -eq 0 ]; then
|
|
164
|
+
echo "[refresh] still building entry points... (${i}s)"
|
|
165
|
+
tail -5 "$LOG" 2>/dev/null | sed 's/^/[refresh:log] /' || true
|
|
166
|
+
fi
|
|
167
|
+
sleep 1
|
|
168
|
+
done
|
|
169
|
+
|
|
170
|
+
all_present=true
|
|
171
|
+
for entry in "${HTML_ENTRIES[@]}"; do
|
|
172
|
+
if [ ! -f "$REPO/${entry}" ]; then
|
|
173
|
+
all_present=false
|
|
174
|
+
echo "[refresh] MISSING: ${entry}"
|
|
175
|
+
fi
|
|
176
|
+
done
|
|
177
|
+
|
|
178
|
+
if ! $all_present; then
|
|
179
|
+
echo "FAIL: rebuild did not restore all entry points within ${TIMEOUT_S}s" >&2
|
|
180
|
+
tail -40 "$LOG" 2>/dev/null || true
|
|
181
|
+
exit 2
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Kill the watcher — leave dist fresh and frozen.
|
|
185
|
+
kill "$BUILD_PID" 2>/dev/null || true
|
|
186
|
+
wait "$BUILD_PID" 2>/dev/null || true
|
|
187
|
+
trap - EXIT
|
|
188
|
+
|
|
189
|
+
echo "[refresh] Build refreshed and frozen — run: recipe reopen"
|
|
190
|
+
echo "[refresh] Log kept at: $LOG"
|