@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.42.0 - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add `app.network_capture` and `app.network_assert` for redacted Mobile HTTP request windows, indexed evidence, and assertions that preserve failed-run diagnostics.
|
|
10
|
+
- Support the same bounded network observation contract on Extension and automatically index safe request metadata with recipe-node boundaries for live Mobile and Extension runs.
|
|
11
|
+
- Add `mm-harness reload` for Metro-equivalent Mobile reloads and in-place Extension CDP refreshes.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Route Mobile console forwarding and recipe bridge commands through the same debugger connection so Network capture can coexist with HUD and wallet/domain actions.
|
|
16
|
+
- Make the canonical Perps performance recipe close positions and orders outside timing for its market-only cohort instead of failing on inherited account state.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Pass Expo `--clear` only for an explicit `mm-harness launch --clear-metro`, bound default Metro concurrency to four workers, and remove a stopped checkout from Watchman's root set.
|
|
21
|
+
- Route React Native DevTools through the harness CDP broker so debugger use, recipe actions, and app reloads no longer evict one another.
|
|
22
|
+
|
|
5
23
|
## 0.41.1 - 2026-08-19
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ mm-harness status
|
|
|
87
87
|
mm-harness stop
|
|
88
88
|
mm-harness logs
|
|
89
89
|
mm-harness debug
|
|
90
|
+
mm-harness reload # Mobile Metro reload / Extension CDP refresh
|
|
90
91
|
mm-harness fixtures set
|
|
91
92
|
|
|
92
93
|
# Delete the current wallet, then reapply the canonical fixture from clean state.
|
|
@@ -109,6 +110,12 @@ mm-harness logs --source app # Mobile app
|
|
|
109
110
|
mm-harness logs --source metro # Mobile bundler
|
|
110
111
|
```
|
|
111
112
|
|
|
113
|
+
Live Mobile and Extension recipes automatically index a bounded, redacted
|
|
114
|
+
`network/run-summary.json` with request timing and recipe-node boundaries. Use
|
|
115
|
+
explicit `app.network_capture` and `app.network_assert` nodes for filtered
|
|
116
|
+
windows and machine-checked request expectations. See
|
|
117
|
+
[Recipe-scoped network capture](docs/NETWORK-CAPTURE.md).
|
|
118
|
+
|
|
112
119
|
## Discover and prove
|
|
113
120
|
|
|
114
121
|
```bash
|
package/adapters/manifest.json
CHANGED
|
@@ -74,6 +74,14 @@
|
|
|
74
74
|
"inputs": "--target --port; env WATCHER_PORT",
|
|
75
75
|
"outputs": "progress on stderr; exit 0 stopped-or-nothing / 1 stop failed / 2 bad args"
|
|
76
76
|
},
|
|
77
|
+
{
|
|
78
|
+
"id": "mobile/reload-app",
|
|
79
|
+
"entry": "adapters/mobile/reload-app.mjs",
|
|
80
|
+
"kind": "node",
|
|
81
|
+
"purpose": "Request an in-place React Native app reload through Metro's protocol-v2 message endpoint.",
|
|
82
|
+
"inputs": "--port --json; env WATCHER_PORT, METRO_PORT",
|
|
83
|
+
"outputs": "reload result on stdout; exit 0 requested / 1 unavailable or invalid input"
|
|
84
|
+
},
|
|
77
85
|
{
|
|
78
86
|
"id": "mobile/prewarm-bundle",
|
|
79
87
|
"entry": "adapters/mobile/prewarm-bundle.sh",
|
|
@@ -19,8 +19,16 @@
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
discoverTarget,
|
|
24
|
+
rankRuntimeCandidates,
|
|
25
|
+
} = require('./lib/target-discovery.cjs');
|
|
23
26
|
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
27
|
+
const {
|
|
28
|
+
brokerSocketPath,
|
|
29
|
+
createBrokerClient,
|
|
30
|
+
deviceIdFromUrl,
|
|
31
|
+
} = require('./lib/cdp-broker.cjs');
|
|
24
32
|
const {
|
|
25
33
|
EXIT_CODE_BY_ERROR_CODE,
|
|
26
34
|
TEACHING_BY_ERROR_CODE,
|
|
@@ -924,6 +932,22 @@ const COMMANDS = {
|
|
|
924
932
|
return result || { count: 0, entries: [] };
|
|
925
933
|
},
|
|
926
934
|
|
|
935
|
+
async 'network-capture-start'(client, args) {
|
|
936
|
+
if (typeof client.control !== 'function') {
|
|
937
|
+
throw new Error('Network capture requires the CDP broker');
|
|
938
|
+
}
|
|
939
|
+
const options = JSON.parse(args[0] || '{}');
|
|
940
|
+
return client.control('capture-start', options);
|
|
941
|
+
},
|
|
942
|
+
|
|
943
|
+
async 'network-capture-end'(client, args) {
|
|
944
|
+
if (typeof client.control !== 'function') {
|
|
945
|
+
throw new Error('Network capture requires the CDP broker');
|
|
946
|
+
}
|
|
947
|
+
const options = JSON.parse(args[0] || '{}');
|
|
948
|
+
return client.control('capture-end', options);
|
|
949
|
+
},
|
|
950
|
+
|
|
927
951
|
};
|
|
928
952
|
|
|
929
953
|
// ---------------------------------------------------------------------------
|
|
@@ -937,6 +961,7 @@ const BRIDGE_LOCK_FILE = path.join(
|
|
|
937
961
|
process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
|
|
938
962
|
'cdp-bridge.lock',
|
|
939
963
|
);
|
|
964
|
+
const BROKER_SOCKET_FILE = brokerSocketPath(path.dirname(BRIDGE_LOCK_FILE));
|
|
940
965
|
const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
|
|
941
966
|
process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
|
|
942
967
|
)
|
|
@@ -1019,6 +1044,8 @@ Commands:
|
|
|
1019
1044
|
issues-arm Install console/exception hooks that
|
|
1020
1045
|
populate globalThis.__AGENTIC_ISSUES__
|
|
1021
1046
|
issues-collect Snapshot + clear the in-app issue buffer
|
|
1047
|
+
network-capture-start <json> Start a broker-owned Network capture
|
|
1048
|
+
network-capture-end <json> End capture and return redacted summary
|
|
1022
1049
|
|
|
1023
1050
|
Environment:
|
|
1024
1051
|
WATCHER_PORT Metro port (default: 8081)
|
|
@@ -1035,24 +1062,54 @@ Environment:
|
|
|
1035
1062
|
process.exit(1);
|
|
1036
1063
|
}
|
|
1037
1064
|
|
|
1038
|
-
// Only a real command takes the debugger slot; help/unknown paths above never
|
|
1039
|
-
// suppress the console forwarder.
|
|
1040
|
-
acquireBridgeLock();
|
|
1041
|
-
|
|
1042
1065
|
const port = loadPort();
|
|
1043
1066
|
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
1067
|
+
const brokerAvailable = fs.existsSync(BROKER_SOCKET_FILE);
|
|
1068
|
+
if (!brokerAvailable) acquireBridgeLock();
|
|
1069
|
+
|
|
1070
|
+
async function brokerTargets() {
|
|
1071
|
+
const response = await fetch(`http://127.0.0.1:${port}/json/list`);
|
|
1072
|
+
if (!response.ok) throw new Error(`Metro target listing returned ${response.status}`);
|
|
1073
|
+
const ranked = rankRuntimeCandidates(await response.json());
|
|
1074
|
+
const picked = new Set();
|
|
1075
|
+
return ranked.flatMap((target) => {
|
|
1076
|
+
const deviceId = deviceIdFromUrl(target.webSocketDebuggerUrl);
|
|
1077
|
+
if (picked.has(deviceId)) return [];
|
|
1078
|
+
picked.add(deviceId);
|
|
1079
|
+
return [{
|
|
1080
|
+
wsUrl: target.webSocketDebuggerUrl,
|
|
1081
|
+
deviceName: target.deviceName || '',
|
|
1082
|
+
}];
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function clientFor(wsUrl) {
|
|
1087
|
+
if (brokerAvailable) {
|
|
1088
|
+
return createBrokerClient(
|
|
1089
|
+
BROKER_SOCKET_FILE,
|
|
1090
|
+
deviceIdFromUrl(wsUrl),
|
|
1091
|
+
timeout,
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (command.startsWith('network-capture-')) {
|
|
1095
|
+
throw new Error('Network capture requires a running CDP broker');
|
|
1096
|
+
}
|
|
1097
|
+
acquireBridgeLock();
|
|
1098
|
+
return createWSClient(wsUrl, timeout);
|
|
1099
|
+
}
|
|
1044
1100
|
|
|
1045
1101
|
// `status` probes ALL connected targets so both platforms are visible.
|
|
1046
1102
|
// `status-selected` falls through to the normal pinned discovery path for
|
|
1047
1103
|
// action code that must survive one device's Hermes runtime rotation.
|
|
1048
1104
|
if (command === 'status') {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1105
|
+
const allTargets = brokerAvailable
|
|
1106
|
+
? await brokerTargets()
|
|
1107
|
+
: await require('./lib/target-discovery.cjs').discoverAllTargets(port);
|
|
1051
1108
|
const results = [];
|
|
1052
1109
|
for (const target of allTargets) {
|
|
1053
1110
|
let client;
|
|
1054
1111
|
try {
|
|
1055
|
-
client = await
|
|
1112
|
+
client = await clientFor(target.wsUrl);
|
|
1056
1113
|
const platform = await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1057
1114
|
const result = await handler(client, args.slice(1), { deviceName: target.deviceName, platform });
|
|
1058
1115
|
results.push(result);
|
|
@@ -1068,8 +1125,11 @@ Environment:
|
|
|
1068
1125
|
return;
|
|
1069
1126
|
}
|
|
1070
1127
|
|
|
1071
|
-
const { wsUrl, deviceName } = await discoverTarget(port
|
|
1072
|
-
|
|
1128
|
+
const { wsUrl, deviceName } = await discoverTarget(port, {
|
|
1129
|
+
probe: !brokerAvailable,
|
|
1130
|
+
});
|
|
1131
|
+
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1132
|
+
const client = await clientFor(wsUrl);
|
|
1073
1133
|
|
|
1074
1134
|
try {
|
|
1075
1135
|
// Detect platform from the running app (exposed by __AGENTIC__ bridge as Platform.OS)
|
|
@@ -29,9 +29,15 @@ const http = require('node:http');
|
|
|
29
29
|
const path = require('node:path');
|
|
30
30
|
const { rankRuntimeCandidates } = require('./lib/target-discovery.cjs');
|
|
31
31
|
const { formatArgs: formatConsoleArgs } = require('./lib/console-format.cjs');
|
|
32
|
+
const {
|
|
33
|
+
brokerSocketPath,
|
|
34
|
+
createCdpBroker,
|
|
35
|
+
deviceIdFromUrl,
|
|
36
|
+
} = require('./lib/cdp-broker.cjs');
|
|
37
|
+
const { createDevtoolsProxy } = require('./lib/devtools-proxy.cjs');
|
|
32
38
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
39
|
+
// Hermes uses Node's built-in WebSocket client. The local DevTools proxy uses
|
|
40
|
+
// the package's pinned `ws` server implementation.
|
|
35
41
|
const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
36
42
|
|
|
37
43
|
const DISCOVER_ACTIVE_MS = 1000; // a device is unattached — look for it quickly
|
|
@@ -101,9 +107,10 @@ function bridgeLockActive() {
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
function yieldSessions() {
|
|
104
|
-
for (const
|
|
110
|
+
for (const session of sessions.values()) {
|
|
111
|
+
broker?.onSessionClose(session.deviceId);
|
|
105
112
|
try {
|
|
106
|
-
ws.close();
|
|
113
|
+
session.ws.close();
|
|
107
114
|
} catch {}
|
|
108
115
|
}
|
|
109
116
|
sessions.clear();
|
|
@@ -170,8 +177,56 @@ function serializeState() {
|
|
|
170
177
|
return JSON.stringify(state);
|
|
171
178
|
}
|
|
172
179
|
|
|
173
|
-
/** deviceId -> live WebSocket session. */
|
|
180
|
+
/** deviceId -> live WebSocket session state. */
|
|
174
181
|
const sessions = new Map();
|
|
182
|
+
let broker = null;
|
|
183
|
+
let devtoolsProxy = null;
|
|
184
|
+
|
|
185
|
+
function sendCommand(session, method, params = {}, timeoutMs = 10_000) {
|
|
186
|
+
return new Promise((resolve, reject) => {
|
|
187
|
+
if (!session.opened) {
|
|
188
|
+
reject(new Error('CDP session is not open'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const id = ++session.nextId;
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
session.pending.delete(id);
|
|
194
|
+
reject(new Error(`CDP command timed out: ${method}`));
|
|
195
|
+
}, timeoutMs);
|
|
196
|
+
session.pending.set(id, {
|
|
197
|
+
resolve: (result) => {
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
resolve(result);
|
|
200
|
+
},
|
|
201
|
+
reject: (error) => {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
reject(error);
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
session.ws.send(JSON.stringify({ id, method, params }));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
broker = createCdpBroker({
|
|
211
|
+
socketPath: brokerSocketPath(path.dirname(out)),
|
|
212
|
+
sessions,
|
|
213
|
+
sendCommand,
|
|
214
|
+
requestDiscovery(deviceId) {
|
|
215
|
+
foreignDebuggerUntil.delete(deviceId);
|
|
216
|
+
discover();
|
|
217
|
+
},
|
|
218
|
+
onClientActivity: noteBridgeCoordination,
|
|
219
|
+
});
|
|
220
|
+
devtoolsProxy = createDevtoolsProxy({
|
|
221
|
+
descriptorPath: path.join(path.dirname(out), 'devtools-proxy.json'),
|
|
222
|
+
sessions,
|
|
223
|
+
sendCommand,
|
|
224
|
+
requestDiscovery: discover,
|
|
225
|
+
allowedOrigins: [
|
|
226
|
+
`http://127.0.0.1:${port}`,
|
|
227
|
+
`http://localhost:${port}`,
|
|
228
|
+
],
|
|
229
|
+
});
|
|
175
230
|
|
|
176
231
|
/** deviceId -> epoch ms until which a foreign debugger owns the slot. */
|
|
177
232
|
const foreignDebuggerUntil = new Map();
|
|
@@ -226,11 +281,6 @@ function deviceNameFromTitle(title) {
|
|
|
226
281
|
return m ? m[1] : (title || 'device');
|
|
227
282
|
}
|
|
228
283
|
|
|
229
|
-
function deviceIdFromUrl(wsUrl) {
|
|
230
|
-
const m = /[?&]device=([^&]+)/.exec(wsUrl || '');
|
|
231
|
-
return m ? m[1] : wsUrl;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
284
|
function connect(target) {
|
|
235
285
|
const deviceId = deviceIdFromUrl(target.webSocketDebuggerUrl);
|
|
236
286
|
const name = deviceNameFromTitle(target.title);
|
|
@@ -240,7 +290,16 @@ function connect(target) {
|
|
|
240
290
|
} catch {
|
|
241
291
|
return;
|
|
242
292
|
}
|
|
243
|
-
|
|
293
|
+
const session = {
|
|
294
|
+
deviceId,
|
|
295
|
+
name,
|
|
296
|
+
ws,
|
|
297
|
+
opened: false,
|
|
298
|
+
brokerReady: false,
|
|
299
|
+
nextId: 1,
|
|
300
|
+
pending: new Map(),
|
|
301
|
+
};
|
|
302
|
+
sessions.set(deviceId, session);
|
|
244
303
|
// The built-in WebSocket has no handshake timeout; a hung connect would hold
|
|
245
304
|
// the session slot forever and block every future re-attach for this device.
|
|
246
305
|
const handshakeTimer = setTimeout(() => {
|
|
@@ -248,10 +307,29 @@ function connect(target) {
|
|
|
248
307
|
ws.close();
|
|
249
308
|
} catch {}
|
|
250
309
|
}, HANDSHAKE_TIMEOUT_MS);
|
|
251
|
-
ws.addEventListener('open', () => {
|
|
310
|
+
ws.addEventListener('open', async () => {
|
|
252
311
|
clearTimeout(handshakeTimer);
|
|
253
|
-
|
|
254
|
-
|
|
312
|
+
session.opened = true;
|
|
313
|
+
try {
|
|
314
|
+
await sendCommand(session, 'Runtime.enable');
|
|
315
|
+
const evaluation = await sendCommand(session, 'Runtime.evaluate', {
|
|
316
|
+
expression: "typeof globalThis.__AGENTIC__ === 'object'",
|
|
317
|
+
returnByValue: true,
|
|
318
|
+
awaitPromise: false,
|
|
319
|
+
});
|
|
320
|
+
if (evaluation?.result?.value !== true) {
|
|
321
|
+
throw new Error('CDP target does not expose __AGENTIC__');
|
|
322
|
+
}
|
|
323
|
+
session.brokerReady = true;
|
|
324
|
+
broker.onSessionOpen(deviceId);
|
|
325
|
+
devtoolsProxy.onSessionOpen(deviceId, session);
|
|
326
|
+
process.stderr.write(`console-forwarder: attached ${name}\n`);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
process.stderr.write(
|
|
329
|
+
`console-forwarder: rejected ${name}: ${String(error?.message || error).slice(0, 256)}\n`,
|
|
330
|
+
);
|
|
331
|
+
ws.close();
|
|
332
|
+
}
|
|
255
333
|
});
|
|
256
334
|
ws.addEventListener('message', (event) => {
|
|
257
335
|
let msg;
|
|
@@ -260,6 +338,17 @@ function connect(target) {
|
|
|
260
338
|
} catch {
|
|
261
339
|
return;
|
|
262
340
|
}
|
|
341
|
+
if (msg.id && session.pending.has(msg.id)) {
|
|
342
|
+
const entry = session.pending.get(msg.id);
|
|
343
|
+
session.pending.delete(msg.id);
|
|
344
|
+
if (msg.error) entry.reject(new Error(JSON.stringify(msg.error)));
|
|
345
|
+
else entry.resolve(msg.result);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (msg.method) {
|
|
349
|
+
broker.onCdpEvent(deviceId, msg.method, msg.params || {});
|
|
350
|
+
devtoolsProxy.onCdpEvent(deviceId, msg.method, msg.params || {});
|
|
351
|
+
}
|
|
263
352
|
if (msg.method !== 'Runtime.consoleAPICalled') return;
|
|
264
353
|
const text = formatArgs(msg.params.args);
|
|
265
354
|
// dev-middleware emits this NOTE on every debugger attach (i.e. ours). Drop it.
|
|
@@ -278,8 +367,15 @@ function connect(target) {
|
|
|
278
367
|
});
|
|
279
368
|
const drop = () => {
|
|
280
369
|
clearTimeout(handshakeTimer);
|
|
281
|
-
|
|
370
|
+
session.opened = false;
|
|
371
|
+
session.brokerReady = false;
|
|
372
|
+
for (const entry of session.pending.values()) {
|
|
373
|
+
entry.reject(new Error('CDP session closed'));
|
|
374
|
+
}
|
|
375
|
+
session.pending.clear();
|
|
376
|
+
if (sessions.get(deviceId) === session) {
|
|
282
377
|
sessions.delete(deviceId);
|
|
378
|
+
broker.onSessionClose(deviceId);
|
|
283
379
|
process.stderr.write(`console-forwarder: detached ${name}; will re-attach\n`);
|
|
284
380
|
}
|
|
285
381
|
};
|
|
@@ -351,10 +447,14 @@ function schedule(ms) {
|
|
|
351
447
|
|
|
352
448
|
process.on('SIGTERM', () => {
|
|
353
449
|
flushSync();
|
|
450
|
+
devtoolsProxy.close();
|
|
451
|
+
broker.close();
|
|
354
452
|
process.exit(0);
|
|
355
453
|
});
|
|
356
454
|
process.on('SIGINT', () => {
|
|
357
455
|
flushSync();
|
|
456
|
+
devtoolsProxy.close();
|
|
457
|
+
broker.close();
|
|
358
458
|
process.exit(0);
|
|
359
459
|
});
|
|
360
460
|
|