@deeeed/metamask-harness 0.41.1 → 0.42.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 +18 -0
- package/README.md +7 -0
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +70 -10
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +115 -15
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +752 -0
- package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
- package/adapters/mobile/reload-app.mjs +67 -0
- package/adapters/mobile/start-console-forwarder.sh +17 -2
- package/adapters/mobile/start-metro.sh +6 -11
- package/adapters/mobile/stop-metro.sh +10 -1
- package/adapters/shared/open-debug.mjs +172 -2
- package/dist/adapters/extension/network-observer.js +300 -0
- package/dist/adapters/mobile/metro-env.js +0 -5
- package/dist/adapters/mobile/prepare.js +1 -3
- package/dist/adapters/mobile/runtime-decision.js +6 -9
- package/dist/adapters.js +14 -1
- package/dist/cli-commands.js +6 -3
- package/dist/cli.js +4 -0
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +45 -20
- package/dist/commands/reload.js +80 -0
- package/dist/commands/run.js +49 -22
- package/dist/mm-harness-cli.js +17 -1
- package/dist/network-observation.js +271 -0
- package/docs/NETWORK-CAPTURE.md +98 -0
- package/docs/RECIPES.md +10 -0
- package/library/actions/mobile/app/network_assert.mjs +14 -0
- package/library/actions/mobile/app/network_capture.mjs +72 -0
- package/library/actions/mobile/platform/bridge.mjs +7 -2
- package/library/actions/shared/app/network-artifact.mjs +10 -0
- package/library/actions/shared/app/network-assert.mjs +154 -0
- package/library/manifests/extension.action-manifest.json +88 -0
- package/library/manifests/mobile.action-manifest.json +107 -0
- package/library/recipes/mobile/perps/performance.recipe.json +11 -11
- package/package.json +1 -1
- package/scripts/completions.sh +2 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import brokerModule from "../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs";
|
|
4
|
+
const DEVICE_ID = "extension";
|
|
5
|
+
const COMMAND_TIMEOUT_MS = 1e4;
|
|
6
|
+
const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
|
|
7
|
+
"background_page",
|
|
8
|
+
"other",
|
|
9
|
+
"page",
|
|
10
|
+
"service_worker"
|
|
11
|
+
]);
|
|
12
|
+
const { brokerSocketPath, createBrokerClient, createCdpBroker } = brokerModule;
|
|
13
|
+
async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
|
|
14
|
+
if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
|
|
15
|
+
throw new Error("Extension network observation requires a valid CDP port.");
|
|
16
|
+
}
|
|
17
|
+
const [version, targets] = await Promise.all([
|
|
18
|
+
fetchJson(`http://127.0.0.1:${cdpPort}/json/version`),
|
|
19
|
+
fetchJson(`http://127.0.0.1:${cdpPort}/json`)
|
|
20
|
+
]);
|
|
21
|
+
const browserWebSocketUrl = String(
|
|
22
|
+
asRecord(version).webSocketDebuggerUrl ?? ""
|
|
23
|
+
);
|
|
24
|
+
const extensionId = extensionIdFromTargets(targets);
|
|
25
|
+
if (!browserWebSocketUrl || !extensionId) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Extension CDP browser or loaded extension target is unavailable."
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const socket = new WebSocket(browserWebSocketUrl);
|
|
31
|
+
await waitForOpen(socket);
|
|
32
|
+
let nextId = 0;
|
|
33
|
+
let closed = false;
|
|
34
|
+
const brokerRef = {};
|
|
35
|
+
const pending = /* @__PURE__ */ new Map();
|
|
36
|
+
const targetSessions = /* @__PURE__ */ new Map();
|
|
37
|
+
const attaching = /* @__PURE__ */ new Set();
|
|
38
|
+
const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => new Promise((resolve, reject) => {
|
|
39
|
+
const id = ++nextId;
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
pending.delete(id);
|
|
42
|
+
reject(new Error(`Extension CDP command timed out: ${method}`));
|
|
43
|
+
}, timeoutMs);
|
|
44
|
+
pending.set(id, { resolve, reject, timer });
|
|
45
|
+
socket.send(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
id,
|
|
48
|
+
method,
|
|
49
|
+
params,
|
|
50
|
+
...sessionId ? { sessionId } : {}
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
const relevantTarget = (target) => {
|
|
55
|
+
if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
|
|
56
|
+
try {
|
|
57
|
+
return new URL(String(target.url)).hostname === extensionId;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const refreshBrokerSession = () => {
|
|
63
|
+
if (!brokerRef.current) return;
|
|
64
|
+
brokerRef.current.onSessionClose(DEVICE_ID);
|
|
65
|
+
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
66
|
+
};
|
|
67
|
+
const attachTarget = async (target) => {
|
|
68
|
+
if (closed || !relevantTarget(target) || targetSessions.has(target.targetId) || attaching.has(target.targetId)) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
attaching.add(target.targetId);
|
|
72
|
+
try {
|
|
73
|
+
const result = asRecord(
|
|
74
|
+
await send("Target.attachToTarget", {
|
|
75
|
+
targetId: target.targetId,
|
|
76
|
+
flatten: true
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
const sessionId = String(result.sessionId ?? "");
|
|
80
|
+
if (sessionId) {
|
|
81
|
+
targetSessions.set(target.targetId, sessionId);
|
|
82
|
+
refreshBrokerSession();
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!/already attached/iu.test(String(error))) {
|
|
86
|
+
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
attaching.delete(target.targetId);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
socket.on("message", (data) => {
|
|
93
|
+
let message;
|
|
94
|
+
try {
|
|
95
|
+
message = JSON.parse(String(data));
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (message.id && pending.has(message.id)) {
|
|
100
|
+
const command = pending.get(message.id);
|
|
101
|
+
pending.delete(message.id);
|
|
102
|
+
if (!command) return;
|
|
103
|
+
clearTimeout(command.timer);
|
|
104
|
+
if (message.error) {
|
|
105
|
+
command.reject(
|
|
106
|
+
new Error(
|
|
107
|
+
String(message.error.message ?? "Extension CDP command failed")
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
} else {
|
|
111
|
+
command.resolve(message.result ?? {});
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (message.method === "Target.targetCreated") {
|
|
116
|
+
const target = asTargetInfo(message.params?.targetInfo);
|
|
117
|
+
if (target) void attachTarget(target);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (message.method === "Target.attachedToTarget") {
|
|
121
|
+
const target = asTargetInfo(message.params?.targetInfo);
|
|
122
|
+
const sessionId = String(message.params?.sessionId ?? "");
|
|
123
|
+
if (target && sessionId && relevantTarget(target)) {
|
|
124
|
+
targetSessions.set(target.targetId, sessionId);
|
|
125
|
+
refreshBrokerSession();
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (message.method === "Target.detachedFromTarget") {
|
|
130
|
+
const sessionId = String(message.params?.sessionId ?? "");
|
|
131
|
+
for (const [targetId, activeSessionId] of targetSessions) {
|
|
132
|
+
if (activeSessionId !== sessionId) continue;
|
|
133
|
+
targetSessions.delete(targetId);
|
|
134
|
+
refreshBrokerSession();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (message.sessionId && message.method?.startsWith("Network.")) {
|
|
139
|
+
brokerRef.current?.onCdpEvent(
|
|
140
|
+
DEVICE_ID,
|
|
141
|
+
message.method,
|
|
142
|
+
message.params ?? {}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
socket.on("close", () => {
|
|
147
|
+
if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
148
|
+
for (const command of pending.values()) {
|
|
149
|
+
clearTimeout(command.timer);
|
|
150
|
+
command.reject(new Error("Extension browser CDP connection closed"));
|
|
151
|
+
}
|
|
152
|
+
pending.clear();
|
|
153
|
+
});
|
|
154
|
+
await send("Target.setDiscoverTargets", { discover: true });
|
|
155
|
+
const targetResult = asRecord(await send("Target.getTargets"));
|
|
156
|
+
const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
|
|
157
|
+
await Promise.all(
|
|
158
|
+
targetInfos.map(asTargetInfo).filter((target) => Boolean(target)).map(attachTarget)
|
|
159
|
+
);
|
|
160
|
+
if (targetSessions.size === 0) {
|
|
161
|
+
socket.close();
|
|
162
|
+
throw new Error("Extension CDP exposes no attachable extension targets.");
|
|
163
|
+
}
|
|
164
|
+
const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
|
|
165
|
+
const socketPath = brokerSocketPath(
|
|
166
|
+
path.join(runtimeDir, "extension-network")
|
|
167
|
+
);
|
|
168
|
+
brokerRef.current = createCdpBroker({
|
|
169
|
+
socketPath,
|
|
170
|
+
sessions,
|
|
171
|
+
async sendCommand(_session, method, params, timeoutMs) {
|
|
172
|
+
const sessionIds = [...new Set(targetSessions.values())];
|
|
173
|
+
if (sessionIds.length === 0) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"Extension CDP has no active extension target sessions."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const results = await Promise.allSettled(
|
|
179
|
+
sessionIds.map(
|
|
180
|
+
(sessionId) => send(method, params, sessionId, timeoutMs)
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
const successes = results.filter(
|
|
184
|
+
(result) => result.status === "fulfilled"
|
|
185
|
+
);
|
|
186
|
+
if (successes.length === 0) {
|
|
187
|
+
const firstFailure = results.find(
|
|
188
|
+
(result) => result.status === "rejected"
|
|
189
|
+
);
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Extension CDP command failed on every target: ${String(firstFailure?.reason ?? method)}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (successes.length !== results.length) {
|
|
195
|
+
results.forEach((result, index) => {
|
|
196
|
+
if (result.status === "fulfilled") return;
|
|
197
|
+
const failedSession = sessionIds[index];
|
|
198
|
+
for (const [targetId, sessionId] of targetSessions) {
|
|
199
|
+
if (sessionId === failedSession) targetSessions.delete(targetId);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
brokerRef.current?.onSessionClose(DEVICE_ID);
|
|
203
|
+
}
|
|
204
|
+
return successes[0].value;
|
|
205
|
+
},
|
|
206
|
+
requestDiscovery() {
|
|
207
|
+
void send("Target.getTargets").then((result) => {
|
|
208
|
+
const infos = asRecord(result).targetInfos;
|
|
209
|
+
if (!Array.isArray(infos)) return;
|
|
210
|
+
for (const value of infos) {
|
|
211
|
+
const target = asTargetInfo(value);
|
|
212
|
+
if (target) void attachTarget(target);
|
|
213
|
+
}
|
|
214
|
+
}).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
brokerRef.current.onSessionOpen(DEVICE_ID);
|
|
218
|
+
await waitForSocket(socketPath);
|
|
219
|
+
const client = await createBrokerClient(
|
|
220
|
+
socketPath,
|
|
221
|
+
DEVICE_ID,
|
|
222
|
+
COMMAND_TIMEOUT_MS
|
|
223
|
+
);
|
|
224
|
+
return {
|
|
225
|
+
start(params) {
|
|
226
|
+
return client.control("capture-start", params, COMMAND_TIMEOUT_MS);
|
|
227
|
+
},
|
|
228
|
+
async end(id) {
|
|
229
|
+
return asRecord(
|
|
230
|
+
await client.control("capture-end", { id }, COMMAND_TIMEOUT_MS)
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
async close() {
|
|
234
|
+
closed = true;
|
|
235
|
+
client.close();
|
|
236
|
+
brokerRef.current?.close();
|
|
237
|
+
socket.close();
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
async function fetchJson(url) {
|
|
242
|
+
const response = await fetch(url);
|
|
243
|
+
if (!response.ok)
|
|
244
|
+
throw new Error(`Extension CDP returned HTTP ${response.status}.`);
|
|
245
|
+
return response.json();
|
|
246
|
+
}
|
|
247
|
+
function extensionIdFromTargets(targets) {
|
|
248
|
+
const values = Array.isArray(targets) ? targets : [];
|
|
249
|
+
for (const target of values) {
|
|
250
|
+
const info = asTargetInfo(target);
|
|
251
|
+
if (!info) continue;
|
|
252
|
+
try {
|
|
253
|
+
const url = new URL(info.url);
|
|
254
|
+
if (url.protocol !== "chrome-extension:") continue;
|
|
255
|
+
const extensionId = url.hostname;
|
|
256
|
+
if (extensionId) return extensionId;
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
function asRecord(value) {
|
|
264
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
265
|
+
}
|
|
266
|
+
function asTargetInfo(value) {
|
|
267
|
+
const target = asRecord(value);
|
|
268
|
+
const targetId = String(target.targetId ?? target.id ?? "");
|
|
269
|
+
const type = String(target.type ?? "");
|
|
270
|
+
const url = String(target.url ?? "");
|
|
271
|
+
return targetId && type && url ? { targetId, type, url } : null;
|
|
272
|
+
}
|
|
273
|
+
function waitForOpen(socket) {
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const timer = setTimeout(() => {
|
|
276
|
+
socket.close();
|
|
277
|
+
reject(new Error("Extension browser CDP connection timed out."));
|
|
278
|
+
}, COMMAND_TIMEOUT_MS);
|
|
279
|
+
socket.once("open", () => {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
resolve();
|
|
282
|
+
});
|
|
283
|
+
socket.once("error", (error) => {
|
|
284
|
+
clearTimeout(timer);
|
|
285
|
+
reject(error);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
async function waitForSocket(socketPath) {
|
|
290
|
+
const { existsSync } = await import("node:fs");
|
|
291
|
+
const deadline = Date.now() + COMMAND_TIMEOUT_MS;
|
|
292
|
+
while (Date.now() < deadline) {
|
|
293
|
+
if (existsSync(socketPath)) return;
|
|
294
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
295
|
+
}
|
|
296
|
+
throw new Error("Extension network observer broker did not start.");
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
createExtensionNetworkObserver
|
|
300
|
+
};
|
|
@@ -32,10 +32,6 @@ function mobileMetroEnvCheck(target) {
|
|
|
32
32
|
status: marker === null ? "missing" : marker.fingerprint === fingerprint ? "current" : "changed"
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
function applyMobileMetroEnvPolicy(target, actions) {
|
|
36
|
-
if (mobileMetroEnvCheck(target).status === "current") return actions;
|
|
37
|
-
return actions.map((action) => action.id === "start-metro" && !action.argv?.includes("--clear") ? { ...action, argv: [...action.argv ?? [], "--clear"] } : action);
|
|
38
|
-
}
|
|
39
35
|
function recordMobileMetroEnvBaseline(target, expectedFingerprint) {
|
|
40
36
|
if (mobileMetroEnvFingerprint(target) !== expectedFingerprint) return false;
|
|
41
37
|
const marker = recipeRuntimePath(target, BASELINE_FILE);
|
|
@@ -69,7 +65,6 @@ function readBaseline(target) {
|
|
|
69
65
|
}
|
|
70
66
|
}
|
|
71
67
|
export {
|
|
72
|
-
applyMobileMetroEnvPolicy,
|
|
73
68
|
mobileMetroEnvCheck,
|
|
74
69
|
mobileMetroEnvFingerprint,
|
|
75
70
|
recordMobileMetroEnvBaseline
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
launchActions
|
|
10
10
|
} from "./runtime-decision.js";
|
|
11
11
|
import {
|
|
12
|
-
applyMobileMetroEnvPolicy,
|
|
13
12
|
mobileMetroEnvCheck,
|
|
14
13
|
recordMobileMetroEnvBaseline
|
|
15
14
|
} from "./metro-env.js";
|
|
@@ -241,9 +240,8 @@ function withAppRestart(actions, restartApp) {
|
|
|
241
240
|
}
|
|
242
241
|
async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode, watcherPort) {
|
|
243
242
|
const resolved = path.resolve(target);
|
|
244
|
-
const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
|
|
245
243
|
let pendingMetroEnvFingerprint;
|
|
246
|
-
for (const action of
|
|
244
|
+
for (const action of requestedActions) {
|
|
247
245
|
if (action.id === "start-metro") {
|
|
248
246
|
pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
|
|
249
247
|
}
|
|
@@ -13,7 +13,7 @@ import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe
|
|
|
13
13
|
import { recipeRuntimePath } from "../../paths.js";
|
|
14
14
|
import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
15
15
|
import { mobileProductMarkers } from "./deps-markers.js";
|
|
16
|
-
import {
|
|
16
|
+
import { mobileMetroEnvCheck } from "./metro-env.js";
|
|
17
17
|
const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
|
|
18
18
|
const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u;
|
|
19
19
|
const NATIVE_MODULE_STALE = /\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
|
|
@@ -130,13 +130,10 @@ async function decideMobileReadiness(target, options = {}) {
|
|
|
130
130
|
decision: "launch",
|
|
131
131
|
reasonCode: report.decision === "ready" ? env.status === "missing" ? "metro-env-baseline-missing" : "metro-env-changed" : report.reasonCode,
|
|
132
132
|
reasons: [
|
|
133
|
-
env.status === "missing" ? "Metro environment has no successful bundle baseline; restart
|
|
133
|
+
env.status === "missing" ? "Metro environment has no successful bundle baseline; restart Metro to load the current environment without clearing its transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart Metro without clearing its transform cache. Pass --clear-metro only if stale transforms are observed.",
|
|
134
134
|
...report.reasons
|
|
135
135
|
],
|
|
136
|
-
actions:
|
|
137
|
-
resolved,
|
|
138
|
-
report.decision === "ready" ? launchActions(resolved) : report.actions
|
|
139
|
-
)
|
|
136
|
+
actions: report.decision === "ready" ? launchActions(resolved) : report.actions
|
|
140
137
|
};
|
|
141
138
|
}
|
|
142
139
|
async function computeMobileReadiness(resolved, options, fast) {
|
|
@@ -216,7 +213,7 @@ async function computeMobileReadiness(resolved, options, fast) {
|
|
|
216
213
|
checks,
|
|
217
214
|
actions: [
|
|
218
215
|
{ id: "rebuild-native-dev-client" },
|
|
219
|
-
...launchActions(resolved
|
|
216
|
+
...launchActions(resolved)
|
|
220
217
|
]
|
|
221
218
|
};
|
|
222
219
|
}
|
|
@@ -231,11 +228,11 @@ async function computeMobileReadiness(resolved, options, fast) {
|
|
|
231
228
|
decision: "launch",
|
|
232
229
|
reasonCode: metroLog.reason === "stale-bundle-error" ? "metro-stale-log" : "bundle-errors-recoverable",
|
|
233
230
|
reasons: [
|
|
234
|
-
metroLog.reason === "stale-bundle-error" ? "Metro log shows prior bundle failures but cited modules are installed; restart Metro
|
|
231
|
+
metroLog.reason === "stale-bundle-error" ? "Metro log shows prior bundle failures but cited modules are installed; restart Metro without clearing the cache. Pass --clear-metro if the failure persists." : "Metro bundle is failing with current deps; restart Metro without clearing the cache. Pass --clear-metro if the failure persists.",
|
|
235
232
|
...excerpt ? [excerpt] : []
|
|
236
233
|
],
|
|
237
234
|
checks,
|
|
238
|
-
actions: launchActions(resolved
|
|
235
|
+
actions: launchActions(resolved)
|
|
239
236
|
};
|
|
240
237
|
}
|
|
241
238
|
return {
|
package/dist/adapters.js
CHANGED
|
@@ -12,6 +12,7 @@ import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.m
|
|
|
12
12
|
import { nativeAgentDeviceStateDir } from "../library/actions/mobile/platform/native-session-name.mjs";
|
|
13
13
|
import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
|
|
14
14
|
import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
|
|
15
|
+
import { handleRunNetworkAction } from "./network-observation.js";
|
|
15
16
|
const execFileAsync = promisify(execFile);
|
|
16
17
|
const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
|
|
17
18
|
"ui.press",
|
|
@@ -103,7 +104,11 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
103
104
|
"metamask.wallet.list_accounts",
|
|
104
105
|
"metamask.wallet.read_state"
|
|
105
106
|
]);
|
|
106
|
-
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
107
|
+
const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
|
|
108
|
+
"ui.navigate",
|
|
109
|
+
"app.network_capture",
|
|
110
|
+
"app.network_assert"
|
|
111
|
+
]);
|
|
107
112
|
function requiresLiveAdapter(platform, action) {
|
|
108
113
|
return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
|
|
109
114
|
}
|
|
@@ -140,6 +145,13 @@ function liveAdapterPathHint(platform, action) {
|
|
|
140
145
|
return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
|
|
141
146
|
}
|
|
142
147
|
async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
|
|
148
|
+
const networkAction = await handleRunNetworkAction(
|
|
149
|
+
platform,
|
|
150
|
+
action,
|
|
151
|
+
node,
|
|
152
|
+
context
|
|
153
|
+
);
|
|
154
|
+
if (networkAction) return networkAction;
|
|
143
155
|
const live = await runLiveFirst(
|
|
144
156
|
platform,
|
|
145
157
|
action,
|
|
@@ -253,6 +265,7 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
|
|
|
253
265
|
];
|
|
254
266
|
const bundledActions = [
|
|
255
267
|
...walletActions,
|
|
268
|
+
...platform !== "core" ? ["app.network_capture", "app.network_assert"] : [],
|
|
256
269
|
"metamask.assets.read_visible_state",
|
|
257
270
|
"metamask.assets.open_details",
|
|
258
271
|
"metamask.assets.read_details",
|
package/dist/cli-commands.js
CHANGED
|
@@ -15,6 +15,7 @@ const SPEC = {
|
|
|
15
15
|
{ name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
|
|
16
16
|
{ name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
|
|
17
17
|
{ name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
|
|
18
|
+
{ name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
|
|
18
19
|
{ name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
|
|
19
20
|
{ name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
|
|
20
21
|
{ name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
|
|
@@ -31,10 +32,11 @@ const SPEC = {
|
|
|
31
32
|
{ name: "ready", aliases: ["ensure-ready"], desc: "Ensure one healthy extension home tab" },
|
|
32
33
|
{ name: "watch", aliases: ["start-watch", "watcher"], desc: "Start/reuse webpack watcher", flags: ["--full", "-f"] },
|
|
33
34
|
{ name: "stop", aliases: ["stop-watch", "stop-watcher"], desc: "Stop this checkout watcher" },
|
|
34
|
-
{ name: "rebuild", aliases: ["reset", "reopen", "
|
|
35
|
+
{ name: "rebuild", aliases: ["reset", "reopen", "browser", "launch", "full-launch", "runtime-launch", "build", "build:once", "build-once", "refresh-once"], desc: "Clean webpack + browser launch", flags: ["--json", "--dry-run", "--full"] },
|
|
35
36
|
{ name: "sidepanel", desc: "Side panel helper", args: ["cycle", "open", "close", "toggle", "status"] },
|
|
36
37
|
{ name: "prepare", flags: ["--target", "--cdp-port", "--runtime-dir", "--validate"] },
|
|
37
|
-
{ name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] }
|
|
38
|
+
{ name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] },
|
|
39
|
+
{ name: "reload", desc: "Reload the live Extension UI over CDP", flags: ["--json"] }
|
|
38
40
|
],
|
|
39
41
|
mobile: [
|
|
40
42
|
{ name: "ios", aliases: ["start"], desc: "Start Metro + launch iOS dev client" },
|
|
@@ -53,7 +55,8 @@ const SPEC = {
|
|
|
53
55
|
{ name: "screenshot", desc: "Capture simulator/device screenshot", args: ["path"] },
|
|
54
56
|
{ name: "dev-menu", aliases: ["devmenu"], desc: "Open RN developer menu", flags: ["--json", "--no-open"] },
|
|
55
57
|
{ name: "prepare", flags: ["--target", "--platform", "--preflight-mode", "--port", "--simulator", "--adb-serial", "--runtime-dir", "--wallet-setup", "--wallet-fixture"] },
|
|
56
|
-
{ name: "debug", flags: ["--json", "--no-open", "--action"] }
|
|
58
|
+
{ name: "debug", flags: ["--json", "--no-open", "--action"] },
|
|
59
|
+
{ name: "reload", desc: "Reload the app without restarting Metro", flags: ["--json"] }
|
|
57
60
|
]
|
|
58
61
|
};
|
|
59
62
|
const GLOBAL_FLAGS = ["--json", "--dry-run", "--full", "-f", "--help", "-h", "--no-open", "--no-color"];
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { handleCompletionCandidates, invalidateCompletionCache } from "./command
|
|
|
15
15
|
import { handleLaunch } from "./commands/launch/index.js";
|
|
16
16
|
import { handleLogs } from "./commands/logs.js";
|
|
17
17
|
import { handleDebug } from "./commands/debug.js";
|
|
18
|
+
import { handleReload } from "./commands/reload.js";
|
|
18
19
|
import { handleFixtures } from "./commands/fixtures.js";
|
|
19
20
|
import { handleRecipeQuality } from "./commands/recipe-quality.js";
|
|
20
21
|
import { handleStatus } from "./commands/status.js";
|
|
@@ -47,6 +48,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
|
|
|
47
48
|
mm-harness logs
|
|
48
49
|
debug Open the debug console (extension DevTools / mobile RN).
|
|
49
50
|
mm-harness debug
|
|
51
|
+
reload Reload the connected Mobile or Extension runtime.
|
|
52
|
+
mm-harness reload
|
|
50
53
|
fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
|
|
51
54
|
mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
|
|
52
55
|
|
|
@@ -118,6 +121,7 @@ async function main(argv) {
|
|
|
118
121
|
if (command === "stop") return handleStop(argv.slice(1));
|
|
119
122
|
if (command === "logs") return handleLogs(argv.slice(1));
|
|
120
123
|
if (command === "debug") return handleDebug(argv.slice(1));
|
|
124
|
+
if (command === "reload") return handleReload(argv.slice(1));
|
|
121
125
|
if (command === "fixtures") return handleFixtures(argv.slice(1));
|
|
122
126
|
if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
|
|
123
127
|
if (command === "check") return handleCheck(argv.slice(1));
|
package/dist/command-contract.js
CHANGED
package/dist/commands/call.js
CHANGED
|
@@ -43,6 +43,9 @@ import {
|
|
|
43
43
|
import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
|
|
44
44
|
import { acquireCheckoutLock } from "../checkout-lock.js";
|
|
45
45
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
46
|
+
import {
|
|
47
|
+
startRunNetworkObservation
|
|
48
|
+
} from "../network-observation.js";
|
|
46
49
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
47
50
|
import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
48
51
|
import { closest } from "../command-contract.js";
|
|
@@ -209,12 +212,14 @@ async function handleCall(argv) {
|
|
|
209
212
|
recordCommandEvidence(artifactsDir);
|
|
210
213
|
const requestedRuntimeOptions = runtimeOptionsFromCli(options);
|
|
211
214
|
const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
|
|
215
|
+
let networkObservation;
|
|
212
216
|
const callRuntimeOptions = {
|
|
213
217
|
...requestedRuntimeOptions,
|
|
214
218
|
...librarySources ? { librarySources } : {},
|
|
215
219
|
autoHud: false,
|
|
216
220
|
suppressLibraryResolutionLogs: true,
|
|
217
221
|
stdoutIsMachineContract: json,
|
|
222
|
+
onActionEvent: ({ nodeId, action, status }) => networkObservation?.onActionEvent({ nodeId, action, status }),
|
|
218
223
|
...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
|
|
219
224
|
source: {
|
|
220
225
|
kind: "operator",
|
|
@@ -275,29 +280,49 @@ async function handleCall(argv) {
|
|
|
275
280
|
});
|
|
276
281
|
if (typeof prepared === "number") return prepared;
|
|
277
282
|
const { state, heal } = prepared;
|
|
278
|
-
|
|
279
|
-
() => {
|
|
280
|
-
const execution = preflightedExecution;
|
|
281
|
-
preflightedExecution = void 0;
|
|
282
|
-
return runRecipe(
|
|
283
|
-
adapter,
|
|
284
|
-
recipe,
|
|
285
|
-
artifactsDir,
|
|
286
|
-
target,
|
|
287
|
-
actionManifestOverride,
|
|
288
|
-
callRuntimeOptions,
|
|
289
|
-
execution
|
|
290
|
-
);
|
|
291
|
-
},
|
|
283
|
+
networkObservation = await startRunNetworkObservation(
|
|
292
284
|
adapter,
|
|
293
285
|
target,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
cdpPort:
|
|
298
|
-
watcherPort:
|
|
299
|
-
}
|
|
286
|
+
artifactsDir,
|
|
287
|
+
process.env,
|
|
288
|
+
{
|
|
289
|
+
cdpPort: callRuntimeOptions.cdpPort,
|
|
290
|
+
watcherPort: callRuntimeOptions.watcherPort
|
|
291
|
+
}
|
|
300
292
|
);
|
|
293
|
+
let executionResult;
|
|
294
|
+
try {
|
|
295
|
+
executionResult = await executeWithHealBounds(
|
|
296
|
+
() => {
|
|
297
|
+
const execution = preflightedExecution;
|
|
298
|
+
preflightedExecution = void 0;
|
|
299
|
+
return runRecipe(
|
|
300
|
+
adapter,
|
|
301
|
+
recipe,
|
|
302
|
+
artifactsDir,
|
|
303
|
+
target,
|
|
304
|
+
actionManifestOverride,
|
|
305
|
+
callRuntimeOptions,
|
|
306
|
+
execution
|
|
307
|
+
);
|
|
308
|
+
},
|
|
309
|
+
adapter,
|
|
310
|
+
target,
|
|
311
|
+
heal,
|
|
312
|
+
state,
|
|
313
|
+
() => recoverRunInfra(adapter, target, json, {
|
|
314
|
+
cdpPort: optionString(options, "cdpPort"),
|
|
315
|
+
watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
|
|
316
|
+
})
|
|
317
|
+
);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
await networkObservation?.finalize();
|
|
320
|
+
networkObservation = void 0;
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
const { result, violation } = executionResult;
|
|
324
|
+
await networkObservation?.finalize(result.artifactManifestPath);
|
|
325
|
+
networkObservation = void 0;
|
|
301
326
|
if (violation !== null) {
|
|
302
327
|
const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
|
|
303
328
|
const example = describedAction ? actionExampleCommand(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runnerDir } from "../paths.js";
|
|
3
|
+
import { getAdapterSurface } from "../adapters/surface.js";
|
|
4
|
+
import {
|
|
5
|
+
ADAPTER_DETECT_NEXT,
|
|
6
|
+
EXIT,
|
|
7
|
+
flag,
|
|
8
|
+
parseFlags,
|
|
9
|
+
resolveAdapter,
|
|
10
|
+
spawnScript,
|
|
11
|
+
targetOf,
|
|
12
|
+
usageOut
|
|
13
|
+
} from "./shared.js";
|
|
14
|
+
const RELOAD_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
|
|
15
|
+
async function handleReload(argv) {
|
|
16
|
+
const { options } = parseFlags(argv, RELOAD_BOOLEANS);
|
|
17
|
+
const json = flag(options, "json");
|
|
18
|
+
const target = targetOf(options);
|
|
19
|
+
const adapter = resolveAdapter(options, target);
|
|
20
|
+
if (!adapter) {
|
|
21
|
+
return usageOut(
|
|
22
|
+
json,
|
|
23
|
+
"reload",
|
|
24
|
+
`could not detect the MetaMask repo type for ${target}`,
|
|
25
|
+
ADAPTER_DETECT_NEXT
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (adapter === "core") {
|
|
29
|
+
return usageOut(
|
|
30
|
+
json,
|
|
31
|
+
"reload",
|
|
32
|
+
"core is headless; there is no app runtime to reload.",
|
|
33
|
+
"mm-harness run <core-recipe>"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
getAdapterSurface(adapter).resolveSlotPorts(target);
|
|
37
|
+
if (adapter === "extension") {
|
|
38
|
+
if (!process.env.CDP_PORT) {
|
|
39
|
+
return usageOut(
|
|
40
|
+
json,
|
|
41
|
+
"reload",
|
|
42
|
+
"the Extension CDP port could not be resolved.",
|
|
43
|
+
"mm-harness launch"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const script2 = path.join(runnerDir, "adapters/extension/reattach.sh");
|
|
47
|
+
const args2 = ["--target", target, "--cdp-port", process.env.CDP_PORT];
|
|
48
|
+
if (process.env.WATCHER_PORT) {
|
|
49
|
+
args2.push("--watcher-port", process.env.WATCHER_PORT);
|
|
50
|
+
}
|
|
51
|
+
const result2 = spawnScript(script2, args2, target, json);
|
|
52
|
+
if (json) {
|
|
53
|
+
console.log(
|
|
54
|
+
JSON.stringify(
|
|
55
|
+
{
|
|
56
|
+
ok: result2.status === 0,
|
|
57
|
+
adapter,
|
|
58
|
+
method: "cdp-reattach",
|
|
59
|
+
command: "reload",
|
|
60
|
+
cdpPort: Number(process.env.CDP_PORT),
|
|
61
|
+
...result2.status === 0 ? {} : { error: result2.output.slice(-4e3) || "Extension reload failed." }
|
|
62
|
+
},
|
|
63
|
+
null,
|
|
64
|
+
2
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return result2.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
69
|
+
}
|
|
70
|
+
const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
|
|
71
|
+
const args = [];
|
|
72
|
+
if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
|
|
73
|
+
if (json) args.push("--json");
|
|
74
|
+
const result = spawnScript(process.execPath, [script, ...args], target, json);
|
|
75
|
+
if (json) process.stdout.write(result.output);
|
|
76
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
handleReload
|
|
80
|
+
};
|