@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -0
  3. package/adapters/manifest.json +8 -0
  4. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +70 -10
  5. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +115 -15
  6. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +752 -0
  7. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
  8. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
  9. package/adapters/mobile/reload-app.mjs +67 -0
  10. package/adapters/mobile/start-console-forwarder.sh +17 -2
  11. package/adapters/mobile/start-metro.sh +6 -11
  12. package/adapters/mobile/stop-metro.sh +10 -1
  13. package/adapters/shared/open-debug.mjs +172 -2
  14. package/dist/adapters/extension/network-observer.js +300 -0
  15. package/dist/adapters/mobile/metro-env.js +0 -5
  16. package/dist/adapters/mobile/prepare.js +1 -3
  17. package/dist/adapters/mobile/runtime-decision.js +6 -9
  18. package/dist/adapters.js +14 -1
  19. package/dist/cli-commands.js +6 -3
  20. package/dist/cli.js +4 -0
  21. package/dist/command-contract.js +3 -0
  22. package/dist/commands/call.js +45 -20
  23. package/dist/commands/reload.js +80 -0
  24. package/dist/commands/run.js +49 -22
  25. package/dist/mm-harness-cli.js +17 -1
  26. package/dist/network-observation.js +271 -0
  27. package/docs/NETWORK-CAPTURE.md +98 -0
  28. package/docs/RECIPES.md +10 -0
  29. package/library/actions/mobile/app/network_assert.mjs +14 -0
  30. package/library/actions/mobile/app/network_capture.mjs +72 -0
  31. package/library/actions/mobile/platform/bridge.mjs +7 -2
  32. package/library/actions/shared/app/network-artifact.mjs +10 -0
  33. package/library/actions/shared/app/network-assert.mjs +154 -0
  34. package/library/manifests/extension.action-manifest.json +88 -0
  35. package/library/manifests/mobile.action-manifest.json +107 -0
  36. package/library/recipes/mobile/perps/performance.recipe.json +11 -11
  37. package/package.json +1 -1
  38. 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 actions) {
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 { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
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 once with a cleared transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart once with a cleared transform cache.",
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: applyMobileMetroEnvPolicy(
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, true)
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 with a cleared cache." : "Metro bundle is failing with current deps; restart Metro (clear cache) before relaunching the dev client.",
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, true)
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(["ui.navigate"]);
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",
@@ -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", "reload", "browser", "launch", "full-launch", "runtime-launch", "build", "build:once", "build-once", "refresh-once"], desc: "Clean webpack + browser launch", flags: ["--json", "--dry-run", "--full"] },
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));
@@ -278,6 +278,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
278
278
  "--no-open": bool()
279
279
  })
280
280
  },
281
+ reload: {
282
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM)
283
+ },
281
284
  update: {
282
285
  options: options(HELP, JSON, { "--check": bool() })
283
286
  },
@@ -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
- const { result, violation } = await executeWithHealBounds(
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
- heal,
295
- state,
296
- () => recoverRunInfra(adapter, target, json, {
297
- cdpPort: optionString(options, "cdpPort"),
298
- watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
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
+ };