react-native-debug-toolkit 2.2.0 → 3.0.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/README.md +5 -8
- package/README.zh-CN.md +5 -8
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/features/network/index.js +32 -12
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +164 -201
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +59 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +529 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/autoDetectDaemon.js +141 -0
- package/lib/commonjs/utils/autoDetectDaemon.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/daemonConnection.js +81 -0
- package/lib/commonjs/utils/daemonConnection.js.map +1 -0
- package/lib/commonjs/utils/daemonSettings.js +110 -0
- package/lib/commonjs/utils/daemonSettings.js.map +1 -0
- package/lib/commonjs/utils/reportToDaemon.js +112 -0
- package/lib/commonjs/utils/reportToDaemon.js.map +1 -0
- package/lib/commonjs/utils/sessionReport.js +132 -0
- package/lib/commonjs/utils/sessionReport.js.map +1 -0
- package/lib/commonjs/utils/streamToDaemon.js +334 -0
- package/lib/commonjs/utils/streamToDaemon.js.map +1 -0
- package/lib/module/features/network/index.js +30 -12
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +163 -200
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +524 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/autoDetectDaemon.js +136 -0
- package/lib/module/utils/autoDetectDaemon.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/daemonConnection.js +77 -0
- package/lib/module/utils/daemonConnection.js.map +1 -0
- package/lib/module/utils/daemonSettings.js +102 -0
- package/lib/module/utils/daemonSettings.js.map +1 -0
- package/lib/module/utils/reportToDaemon.js +105 -0
- package/lib/module/utils/reportToDaemon.js.map +1 -0
- package/lib/module/utils/sessionReport.js +128 -0
- package/lib/module/utils/sessionReport.js.map +1 -0
- package/lib/module/utils/streamToDaemon.js +328 -0
- package/lib/module/utils/streamToDaemon.js.map +1 -0
- package/lib/typescript/src/features/network/index.d.ts +2 -4
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +2 -15
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +11 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts +15 -0
- package/lib/typescript/src/utils/autoDetectDaemon.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/daemonConnection.d.ts +18 -0
- package/lib/typescript/src/utils/daemonConnection.d.ts.map +1 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts +19 -0
- package/lib/typescript/src/utils/daemonSettings.d.ts.map +1 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts +34 -0
- package/lib/typescript/src/utils/reportToDaemon.d.ts.map +1 -0
- package/lib/typescript/src/utils/sessionReport.d.ts +18 -0
- package/lib/typescript/src/utils/sessionReport.d.ts.map +1 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts +23 -0
- package/lib/typescript/src/utils/streamToDaemon.d.ts.map +1 -0
- package/node/daemon/src/cli.js +75 -0
- package/node/daemon/src/console/console.html +936 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +32 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +365 -0
- package/node/daemon/src/store.js +110 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +95 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +10 -5
- package/src/features/network/index.ts +35 -19
- package/src/features/network/networkInterceptor.ts +224 -236
- package/src/index.ts +15 -1
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +566 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/autoDetectDaemon.ts +175 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/daemonConnection.ts +133 -0
- package/src/utils/daemonSettings.ts +134 -0
- package/src/utils/reportToDaemon.ts +172 -0
- package/src/utils/sessionReport.ts +203 -0
- package/src/utils/streamToDaemon.ts +419 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function sendUnauthorized(res) {
|
|
7
|
+
const body = 'Unauthorized';
|
|
8
|
+
res.writeHead(401, {
|
|
9
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
10
|
+
'content-length': Buffer.byteLength(body),
|
|
11
|
+
});
|
|
12
|
+
res.end(body);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createConsoleHandler(options = {}) {
|
|
16
|
+
const htmlPath = path.join(__dirname, 'console.html');
|
|
17
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
18
|
+
const htmlBytes = Buffer.byteLength(html);
|
|
19
|
+
const authorize = options.authorize || (() => true);
|
|
20
|
+
|
|
21
|
+
return function handleConsoleRequest(req, res, url, method) {
|
|
22
|
+
if (method === 'GET' && url.pathname === '/') {
|
|
23
|
+
res.writeHead(302, { location: `/console${url.search || ''}` });
|
|
24
|
+
res.end();
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (method === 'GET' && url.pathname === '/console') {
|
|
29
|
+
if (!authorize(req, res, url)) {
|
|
30
|
+
if (!res.headersSent) sendUnauthorized(res);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
res.writeHead(200, {
|
|
35
|
+
'content-type': 'text/html; charset=utf-8',
|
|
36
|
+
'content-length': htmlBytes,
|
|
37
|
+
'cache-control': 'no-store',
|
|
38
|
+
});
|
|
39
|
+
res.end(html);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { createConsoleHandler };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_HOST = '0.0.0.0';
|
|
6
|
+
const DEFAULT_PORT = 3799;
|
|
7
|
+
const DAEMON_NAME = 'react-native-debug-toolkit-daemon';
|
|
8
|
+
const DAEMON_VERSION = '0.1.0';
|
|
9
|
+
const REPORT_PROTOCOL_VERSION = 2;
|
|
10
|
+
|
|
11
|
+
function getLanIPs() {
|
|
12
|
+
const interfaces = os.networkInterfaces();
|
|
13
|
+
const ips = [];
|
|
14
|
+
for (const entries of Object.values(interfaces)) {
|
|
15
|
+
if (!entries) continue;
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.family === 'IPv4' && !entry.internal) {
|
|
18
|
+
ips.push(entry.address);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return ips;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
DAEMON_NAME,
|
|
27
|
+
DAEMON_VERSION,
|
|
28
|
+
DEFAULT_HOST,
|
|
29
|
+
DEFAULT_PORT,
|
|
30
|
+
REPORT_PROTOCOL_VERSION,
|
|
31
|
+
getLanIPs,
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { startDaemonFromCli } = require('./cli');
|
|
4
|
+
const { createDaemonServer } = require('./server');
|
|
5
|
+
const { createMemoryStore } = require('./store');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
createDaemonServer,
|
|
9
|
+
createMemoryStore,
|
|
10
|
+
startDaemonFromCli,
|
|
11
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const { URL } = require('url');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
DAEMON_NAME,
|
|
8
|
+
DAEMON_VERSION,
|
|
9
|
+
REPORT_PROTOCOL_VERSION,
|
|
10
|
+
getLanIPs,
|
|
11
|
+
} = require('./constants');
|
|
12
|
+
const { createMemoryStore } = require('./store');
|
|
13
|
+
const { createConsoleHandler } = require('./console');
|
|
14
|
+
|
|
15
|
+
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const MAX_SSE_CLIENTS = 20;
|
|
17
|
+
|
|
18
|
+
function sendJson(res, statusCode, payload) {
|
|
19
|
+
const body = JSON.stringify(payload);
|
|
20
|
+
res.writeHead(statusCode, {
|
|
21
|
+
'content-type': 'application/json; charset=utf-8',
|
|
22
|
+
'content-length': Buffer.byteLength(body),
|
|
23
|
+
});
|
|
24
|
+
res.end(body);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJsonBody(req) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
let size = 0;
|
|
30
|
+
const chunks = [];
|
|
31
|
+
|
|
32
|
+
req.on('data', (chunk) => {
|
|
33
|
+
size += chunk.length;
|
|
34
|
+
if (size > MAX_BODY_BYTES) {
|
|
35
|
+
reject(Object.assign(new Error('Request body is too large'), { statusCode: 413 }));
|
|
36
|
+
req.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
req.on('end', () => {
|
|
43
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
44
|
+
if (!raw) {
|
|
45
|
+
reject(Object.assign(new Error('Request body is required'), { statusCode: 400 }));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
resolve(JSON.parse(raw));
|
|
51
|
+
} catch {
|
|
52
|
+
reject(Object.assign(new Error('Request body must be valid JSON'), { statusCode: 400 }));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isValidReport(report) {
|
|
61
|
+
return Boolean(
|
|
62
|
+
report &&
|
|
63
|
+
typeof report === 'object' &&
|
|
64
|
+
report.version === REPORT_PROTOCOL_VERSION &&
|
|
65
|
+
report.logs &&
|
|
66
|
+
typeof report.logs === 'object' &&
|
|
67
|
+
!Array.isArray(report.logs),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getBearerToken(req) {
|
|
72
|
+
const header = req.headers.authorization;
|
|
73
|
+
if (!header || Array.isArray(header)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const match = header.match(/^Bearer\s+(.+)$/i);
|
|
78
|
+
return match ? match[1] : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireToken(req, url, token) {
|
|
82
|
+
if (!token) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return getBearerToken(req) === token || url.searchParams.get('token') === token;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function authorizeRequest(req, res, url, token) {
|
|
89
|
+
if (requireToken(req, url, token)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
sendJson(res, 401, { ok: false, error: 'Unauthorized' });
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toSessionPayload(session) {
|
|
98
|
+
if (!session) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
sessionId: session.sessionId,
|
|
104
|
+
receivedAt: session.receivedAt,
|
|
105
|
+
logCount: session.logCount,
|
|
106
|
+
report: session.report,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function selectLogs(session, searchParams) {
|
|
111
|
+
if (!session) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const type = searchParams.get('type');
|
|
116
|
+
const limitParam = Number(searchParams.get('limit') || 50);
|
|
117
|
+
const limit = Number.isFinite(limitParam) && limitParam > 0
|
|
118
|
+
? Math.min(Math.floor(limitParam), 500)
|
|
119
|
+
: 50;
|
|
120
|
+
const failedOnly = searchParams.get('failedOnly') === 'true';
|
|
121
|
+
const logs = session.report.logs || {};
|
|
122
|
+
|
|
123
|
+
let entries = [];
|
|
124
|
+
if (type) {
|
|
125
|
+
entries = Array.isArray(logs[type]) ? logs[type] : [];
|
|
126
|
+
} else {
|
|
127
|
+
entries = Object.values(logs).flatMap((value) => Array.isArray(value) ? value : []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (failedOnly) {
|
|
131
|
+
entries = entries.filter((entry) => (
|
|
132
|
+
entry &&
|
|
133
|
+
typeof entry === 'object' &&
|
|
134
|
+
(
|
|
135
|
+
Boolean(entry.error) ||
|
|
136
|
+
entry.level === 'error' ||
|
|
137
|
+
entry.response?.success === false ||
|
|
138
|
+
entry.response?.status >= 400
|
|
139
|
+
)
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return entries.slice(-limit);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function broadcastSSE(clients, eventType, session, delta) {
|
|
147
|
+
if (clients.size === 0) return;
|
|
148
|
+
|
|
149
|
+
const payload = delta
|
|
150
|
+
? { type: 'delta', sessionId: session.sessionId, delta, logCount: session.logCount }
|
|
151
|
+
: { type: 'full', sessionId: session.sessionId, logCount: session.logCount, device: session.report.device || null };
|
|
152
|
+
const data = JSON.stringify(payload);
|
|
153
|
+
|
|
154
|
+
clients.forEach((client) => {
|
|
155
|
+
try {
|
|
156
|
+
client.write(`event: ${eventType}\ndata: ${data}\n\n`);
|
|
157
|
+
} catch {
|
|
158
|
+
clients.delete(client);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createDaemonServer(options = {}) {
|
|
164
|
+
const sseClients = new Set();
|
|
165
|
+
const maxSseClients = options.maxSseClients || MAX_SSE_CLIENTS;
|
|
166
|
+
let keepaliveTimer = null;
|
|
167
|
+
|
|
168
|
+
const store = options.store || createMemoryStore({
|
|
169
|
+
onUpdate(session, type, delta) {
|
|
170
|
+
broadcastSSE(sseClients, 'logs', session, type === 'delta' ? delta : null);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const token = options.token || null;
|
|
174
|
+
const handleConsole = createConsoleHandler({
|
|
175
|
+
authorize: (req, res, url) => authorizeRequest(req, res, url, token),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const server = http.createServer(async (req, res) => {
|
|
179
|
+
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
180
|
+
const method = req.method || 'GET';
|
|
181
|
+
|
|
182
|
+
if (handleConsole(req, res, url, method)) return;
|
|
183
|
+
|
|
184
|
+
if (method === 'GET' && url.pathname === '/events') {
|
|
185
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
186
|
+
if (sseClients.size >= maxSseClients) {
|
|
187
|
+
sendJson(res, 503, { ok: false, error: 'Too many SSE clients' });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
res.writeHead(200, {
|
|
192
|
+
'Content-Type': 'text/event-stream',
|
|
193
|
+
'Cache-Control': 'no-cache',
|
|
194
|
+
Connection: 'keep-alive',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
sseClients.add(res);
|
|
198
|
+
req.on('close', () => { sseClients.delete(res); });
|
|
199
|
+
|
|
200
|
+
const latest = store.getLatestSession();
|
|
201
|
+
if (latest) {
|
|
202
|
+
const data = JSON.stringify({ type: 'full', sessionId: latest.sessionId, logCount: latest.logCount, device: latest.report.device || null });
|
|
203
|
+
res.write(`event: logs\ndata: ${data}\n\n`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!keepaliveTimer) {
|
|
207
|
+
keepaliveTimer = setInterval(() => {
|
|
208
|
+
if (sseClients.size > 0) {
|
|
209
|
+
sseClients.forEach((client) => {
|
|
210
|
+
try { client.write(':keepalive\n\n'); } catch { sseClients.delete(client); }
|
|
211
|
+
});
|
|
212
|
+
} else {
|
|
213
|
+
clearInterval(keepaliveTimer);
|
|
214
|
+
keepaliveTimer = null;
|
|
215
|
+
}
|
|
216
|
+
}, 30000);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (method === 'GET' && url.pathname === '/health') {
|
|
222
|
+
sendJson(res, 200, {
|
|
223
|
+
ok: true,
|
|
224
|
+
name: DAEMON_NAME,
|
|
225
|
+
version: DAEMON_VERSION,
|
|
226
|
+
protocolVersion: REPORT_PROTOCOL_VERSION,
|
|
227
|
+
ips: getLanIPs(),
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (method === 'POST' && url.pathname === '/report') {
|
|
233
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const report = await readJsonBody(req);
|
|
237
|
+
if (!isValidReport(report)) {
|
|
238
|
+
sendJson(res, 400, {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Report must include version ${REPORT_PROTOCOL_VERSION} and logs object`,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const session = store.saveReport(report);
|
|
246
|
+
sendJson(res, 200, {
|
|
247
|
+
ok: true,
|
|
248
|
+
sessionId: session.sessionId,
|
|
249
|
+
receivedAt: session.receivedAt,
|
|
250
|
+
logCount: session.logCount,
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
sendJson(res, error.statusCode || 500, {
|
|
254
|
+
ok: false,
|
|
255
|
+
error: error.message || 'Failed to read report',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (method === 'POST' && url.pathname === '/ingest') {
|
|
262
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const body = await readJsonBody(req);
|
|
266
|
+
if (!body || typeof body.sessionId !== 'string' || !body.delta) {
|
|
267
|
+
sendJson(res, 400, { ok: false, error: 'Must include sessionId and delta' });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const session = store.appendLogs(body.sessionId, body.delta);
|
|
272
|
+
if (!session) {
|
|
273
|
+
sendJson(res, 404, { ok: false, error: 'Session not found' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
sendJson(res, 200, { ok: true, sessionId: session.sessionId, logCount: session.logCount });
|
|
278
|
+
} catch (error) {
|
|
279
|
+
sendJson(res, error.statusCode || 500, {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: error.message || 'Failed to ingest',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (method === 'GET' && (url.pathname === '/latest' || url.pathname === '/sessions/latest')) {
|
|
288
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
289
|
+
|
|
290
|
+
const session = store.getLatestSession();
|
|
291
|
+
if (!session) {
|
|
292
|
+
sendJson(res, 404, { ok: false, error: 'No debug session report has been received' });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
sendJson(res, 200, {
|
|
297
|
+
ok: true,
|
|
298
|
+
...toSessionPayload(session),
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (method === 'GET' && url.pathname === '/sessions') {
|
|
304
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
305
|
+
|
|
306
|
+
sendJson(res, 200, { ok: true, sessions: store.listSessions() });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (method === 'DELETE' && url.pathname === '/sessions') {
|
|
311
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
312
|
+
|
|
313
|
+
store.clear();
|
|
314
|
+
sendJson(res, 200, { ok: true });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)(?:\/logs)?$/);
|
|
319
|
+
if (method === 'GET' && sessionMatch) {
|
|
320
|
+
if (!authorizeRequest(req, res, url, token)) return;
|
|
321
|
+
|
|
322
|
+
const session = store.getSession(decodeURIComponent(sessionMatch[1]));
|
|
323
|
+
if (!session) {
|
|
324
|
+
sendJson(res, 404, { ok: false, error: 'Session not found' });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (url.pathname.endsWith('/logs')) {
|
|
329
|
+
sendJson(res, 200, {
|
|
330
|
+
ok: true,
|
|
331
|
+
sessionId: session.sessionId,
|
|
332
|
+
receivedAt: session.receivedAt,
|
|
333
|
+
logs: selectLogs(session, url.searchParams),
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
sendJson(res, 200, {
|
|
339
|
+
ok: true,
|
|
340
|
+
...toSessionPayload(session),
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
sendJson(res, 404, { ok: false, error: 'Not found' });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
server.on('close', () => {
|
|
349
|
+
if (keepaliveTimer) {
|
|
350
|
+
clearInterval(keepaliveTimer);
|
|
351
|
+
keepaliveTimer = null;
|
|
352
|
+
}
|
|
353
|
+
sseClients.forEach((client) => {
|
|
354
|
+
try { client.destroy(); } catch {}
|
|
355
|
+
});
|
|
356
|
+
sseClients.clear();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
return { server, store };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = {
|
|
363
|
+
createDaemonServer,
|
|
364
|
+
isValidReport,
|
|
365
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_SESSIONS = 20;
|
|
4
|
+
|
|
5
|
+
function createLogCount(report) {
|
|
6
|
+
const logs = report && typeof report === 'object' ? report.logs : undefined;
|
|
7
|
+
if (!logs || typeof logs !== 'object') {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return Object.entries(logs).reduce((acc, [type, entries]) => {
|
|
12
|
+
if (Array.isArray(entries)) {
|
|
13
|
+
acc[type] = entries.length;
|
|
14
|
+
}
|
|
15
|
+
return acc;
|
|
16
|
+
}, {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createSessionId(now, sequence) {
|
|
20
|
+
return `session_${now}_${sequence}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createMemoryStore(options = {}) {
|
|
24
|
+
const maxSessions = options.maxSessions || DEFAULT_MAX_SESSIONS;
|
|
25
|
+
const onUpdate = options.onUpdate || null;
|
|
26
|
+
const sessions = [];
|
|
27
|
+
let sequence = 0;
|
|
28
|
+
|
|
29
|
+
function saveReport(report) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
sequence += 1;
|
|
32
|
+
|
|
33
|
+
const session = {
|
|
34
|
+
sessionId: typeof report.sessionId === 'string' && report.sessionId
|
|
35
|
+
? report.sessionId
|
|
36
|
+
: createSessionId(now, sequence),
|
|
37
|
+
receivedAt: new Date(now).toISOString(),
|
|
38
|
+
report,
|
|
39
|
+
logCount: createLogCount(report),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const existingIndex = sessions.findIndex((item) => item.sessionId === session.sessionId);
|
|
43
|
+
if (existingIndex >= 0) {
|
|
44
|
+
sessions.splice(existingIndex, 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sessions.push(session);
|
|
48
|
+
while (sessions.length > maxSessions) {
|
|
49
|
+
sessions.shift();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (onUpdate) onUpdate(session, 'full');
|
|
53
|
+
return session;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function appendLogs(sessionId, delta) {
|
|
57
|
+
const session = sessions.find((s) => s.sessionId === sessionId);
|
|
58
|
+
if (!session) return null;
|
|
59
|
+
|
|
60
|
+
const deltaLogs = (delta && delta.logs) || {};
|
|
61
|
+
Object.entries(deltaLogs).forEach(([type, entries]) => {
|
|
62
|
+
if (!Array.isArray(entries)) return;
|
|
63
|
+
if (!session.report.logs[type]) {
|
|
64
|
+
session.report.logs[type] = [];
|
|
65
|
+
}
|
|
66
|
+
session.report.logs[type].push(...entries);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
session.logCount = createLogCount(session.report);
|
|
70
|
+
if (onUpdate) onUpdate(session, 'delta', delta);
|
|
71
|
+
return session;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function listSessions() {
|
|
75
|
+
return sessions
|
|
76
|
+
.slice()
|
|
77
|
+
.reverse()
|
|
78
|
+
.map((session) => ({
|
|
79
|
+
sessionId: session.sessionId,
|
|
80
|
+
receivedAt: session.receivedAt,
|
|
81
|
+
logCount: session.logCount,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getLatestSession() {
|
|
86
|
+
return sessions[sessions.length - 1] || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getSession(sessionId) {
|
|
90
|
+
return sessions.find((session) => session.sessionId === sessionId) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clear() {
|
|
94
|
+
sessions.splice(0, sessions.length);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
appendLogs,
|
|
99
|
+
clear,
|
|
100
|
+
getLatestSession,
|
|
101
|
+
getSession,
|
|
102
|
+
listSessions,
|
|
103
|
+
saveReport,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
createLogCount,
|
|
109
|
+
createMemoryStore,
|
|
110
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ensureDaemon } = require('./daemonClient');
|
|
4
|
+
const { startStdioServer } = require('./server');
|
|
5
|
+
|
|
6
|
+
function startMcpFromCli() {
|
|
7
|
+
let daemonPromise;
|
|
8
|
+
|
|
9
|
+
const ensureDaemonOnce = () => {
|
|
10
|
+
if (!daemonPromise) {
|
|
11
|
+
daemonPromise = ensureDaemon();
|
|
12
|
+
daemonPromise.then((result) => {
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
process.stderr.write(`Debug toolkit daemon unavailable: ${result.error}\n`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return daemonPromise;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
ensureDaemonOnce();
|
|
22
|
+
startStdioServer({
|
|
23
|
+
context: {
|
|
24
|
+
ensureDaemon: ensureDaemonOnce,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
startMcpFromCli,
|
|
31
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MCP_SERVER_NAME = 'react-native-debug-toolkit-mcp';
|
|
4
|
+
const MCP_SERVER_VERSION = '0.1.0';
|
|
5
|
+
const DEFAULT_DAEMON_ORIGIN = 'http://127.0.0.1:3799';
|
|
6
|
+
const EXPECTED_PROTOCOL_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
DEFAULT_DAEMON_ORIGIN,
|
|
10
|
+
EXPECTED_PROTOCOL_VERSION,
|
|
11
|
+
MCP_SERVER_NAME,
|
|
12
|
+
MCP_SERVER_VERSION,
|
|
13
|
+
};
|