dev3000 0.0.21 → 0.0.24

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.
@@ -0,0 +1,351 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { readFileSync, existsSync } from 'fs';
3
+
4
+ interface InteractionEvent {
5
+ timestamp: string;
6
+ type: 'CLICK' | 'TAP' | 'SCROLL' | 'KEY';
7
+ x?: number;
8
+ y?: number;
9
+ target?: string;
10
+ direction?: string;
11
+ distance?: number;
12
+ key?: string;
13
+ url?: string;
14
+ }
15
+
16
+ interface NavigationEvent {
17
+ timestamp: string;
18
+ url: string;
19
+ }
20
+
21
+ interface ScreenshotEvent {
22
+ timestamp: string;
23
+ url: string;
24
+ event: string;
25
+ }
26
+
27
+ interface ReplayData {
28
+ interactions: InteractionEvent[];
29
+ navigations: NavigationEvent[];
30
+ screenshots: ScreenshotEvent[];
31
+ startTime: string;
32
+ endTime: string;
33
+ duration: number;
34
+ }
35
+
36
+ function parseLogFile(logContent: string, startTime?: string, endTime?: string): ReplayData {
37
+ const lines = logContent.split('\n');
38
+ const interactions: InteractionEvent[] = [];
39
+ const navigations: NavigationEvent[] = [];
40
+ const screenshots: ScreenshotEvent[] = [];
41
+
42
+ let actualStartTime = '';
43
+ let actualEndTime = '';
44
+
45
+ for (const line of lines) {
46
+ if (!line.trim()) continue;
47
+
48
+ const timestampMatch = line.match(/^\[([^\]]+)\]/);
49
+ if (!timestampMatch) continue;
50
+
51
+ const timestamp = timestampMatch[1];
52
+ const logTime = new Date(timestamp);
53
+
54
+ // Filter by time range if specified
55
+ if (startTime && logTime < new Date(startTime)) continue;
56
+ if (endTime && logTime > new Date(endTime)) continue;
57
+
58
+ if (!actualStartTime) actualStartTime = timestamp;
59
+ actualEndTime = timestamp;
60
+
61
+ // Parse interaction events (both old and new formats)
62
+ const interactionMatch = line.match(/\[INTERACTION\] (.+)/);
63
+ if (interactionMatch) {
64
+ const data = interactionMatch[1];
65
+
66
+ try {
67
+ // Try parsing as JSON (new format)
68
+ const interactionData = JSON.parse(data);
69
+
70
+ if (interactionData.type === 'CLICK' || interactionData.type === 'TAP') {
71
+ interactions.push({
72
+ timestamp,
73
+ type: interactionData.type as 'CLICK' | 'TAP',
74
+ x: interactionData.coordinates?.x || 0,
75
+ y: interactionData.coordinates?.y || 0,
76
+ target: interactionData.target || 'unknown'
77
+ });
78
+ } else if (interactionData.type === 'SCROLL') {
79
+ interactions.push({
80
+ timestamp,
81
+ type: 'SCROLL',
82
+ direction: interactionData.direction || 'DOWN',
83
+ distance: interactionData.distance || 0,
84
+ x: interactionData.to?.x || 0,
85
+ y: interactionData.to?.y || 0
86
+ });
87
+ } else if (interactionData.type === 'KEY') {
88
+ interactions.push({
89
+ timestamp,
90
+ type: 'KEY',
91
+ key: interactionData.key || 'unknown',
92
+ target: interactionData.target || 'unknown'
93
+ });
94
+ }
95
+ } catch (jsonError) {
96
+ // Fallback to old format parsing
97
+ const oldFormatMatch = data.match(/(CLICK|TAP|SCROLL|KEY) (.+)/);
98
+ if (oldFormatMatch) {
99
+ const [, type, details] = oldFormatMatch;
100
+
101
+ if (type === 'CLICK' || type === 'TAP') {
102
+ const coordMatch = details.match(/at \((\d+), (\d+)\) on (.+)/);
103
+ if (coordMatch) {
104
+ interactions.push({
105
+ timestamp,
106
+ type: type as 'CLICK' | 'TAP',
107
+ x: parseInt(coordMatch[1]),
108
+ y: parseInt(coordMatch[2]),
109
+ target: coordMatch[3]
110
+ });
111
+ }
112
+ } else if (type === 'SCROLL') {
113
+ const scrollMatch = details.match(/(\w+) (\d+)px to \((\d+), (\d+)\)/);
114
+ if (scrollMatch) {
115
+ interactions.push({
116
+ timestamp,
117
+ type: 'SCROLL',
118
+ direction: scrollMatch[1],
119
+ distance: parseInt(scrollMatch[2]),
120
+ x: parseInt(scrollMatch[3]),
121
+ y: parseInt(scrollMatch[4])
122
+ });
123
+ }
124
+ } else if (type === 'KEY') {
125
+ const keyMatch = details.match(/(.+) in (.+)/);
126
+ if (keyMatch) {
127
+ interactions.push({
128
+ timestamp,
129
+ type: 'KEY',
130
+ key: keyMatch[1],
131
+ target: keyMatch[2]
132
+ });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ // Parse navigation events
140
+ const navigationMatch = line.match(/\[NAVIGATION\] (.+)/);
141
+ if (navigationMatch) {
142
+ navigations.push({
143
+ timestamp,
144
+ url: navigationMatch[1]
145
+ });
146
+ }
147
+
148
+ // Parse screenshot events
149
+ const screenshotMatch = line.match(/\[SCREENSHOT\] (.+)/);
150
+ if (screenshotMatch) {
151
+ const urlParts = screenshotMatch[1].split('/');
152
+ const filename = urlParts[urlParts.length - 1];
153
+ const eventType = filename.split('-').slice(3).join('-').replace('.png', '');
154
+
155
+ screenshots.push({
156
+ timestamp,
157
+ url: screenshotMatch[1],
158
+ event: eventType
159
+ });
160
+ }
161
+ }
162
+
163
+ const startTimeMs = new Date(actualStartTime).getTime();
164
+ const endTimeMs = new Date(actualEndTime).getTime();
165
+ const duration = endTimeMs - startTimeMs;
166
+
167
+ return {
168
+ interactions,
169
+ navigations,
170
+ screenshots,
171
+ startTime: actualStartTime,
172
+ endTime: actualEndTime,
173
+ duration
174
+ };
175
+ }
176
+
177
+ export async function GET(request: NextRequest) {
178
+ try {
179
+ const { searchParams } = new URL(request.url);
180
+ const action = searchParams.get('action');
181
+ const startTime = searchParams.get('startTime');
182
+ const endTime = searchParams.get('endTime');
183
+
184
+ // Get log file path from environment
185
+ const logFilePath = process.env.LOG_FILE_PATH || '/tmp/dev3000.log';
186
+
187
+ if (!existsSync(logFilePath)) {
188
+ return NextResponse.json({ error: 'Log file not found' }, { status: 404 });
189
+ }
190
+
191
+ const logContent = readFileSync(logFilePath, 'utf8');
192
+
193
+ if (action === 'parse') {
194
+ // Parse the log file and return replay data
195
+ const replayData = parseLogFile(logContent, startTime || undefined, endTime || undefined);
196
+ return NextResponse.json(replayData);
197
+ }
198
+
199
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
200
+ } catch (error) {
201
+ console.error('Replay API error:', error);
202
+ return NextResponse.json(
203
+ { error: 'Failed to process replay request' },
204
+ { status: 500 }
205
+ );
206
+ }
207
+ }
208
+
209
+ export async function POST(request: NextRequest) {
210
+ try {
211
+ const body = await request.json();
212
+ const { action, replayData, speed = 1 } = body;
213
+
214
+ if (action === 'execute') {
215
+ // Generate CDP commands for replay
216
+ const cdpCommands = generateCDPCommands(replayData, speed);
217
+
218
+ // Try to execute the commands via CDP
219
+ try {
220
+ const result = await executeCDPCommands(cdpCommands);
221
+ return NextResponse.json({
222
+ success: true,
223
+ message: 'Replay executed successfully',
224
+ result: result,
225
+ totalCommands: cdpCommands.length
226
+ });
227
+ } catch (error) {
228
+ // Fallback: return commands for manual execution
229
+ return NextResponse.json({
230
+ success: false,
231
+ message: 'CDP execution failed, returning commands for manual execution',
232
+ commands: cdpCommands,
233
+ error: error instanceof Error ? error.message : 'Unknown error'
234
+ });
235
+ }
236
+ }
237
+
238
+ return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
239
+ } catch (error) {
240
+ console.error('Replay execution error:', error);
241
+ return NextResponse.json(
242
+ { error: 'Failed to execute replay' },
243
+ { status: 500 }
244
+ );
245
+ }
246
+ }
247
+
248
+ interface CDPCommand {
249
+ method: string;
250
+ params: any;
251
+ delay: number;
252
+ description: string;
253
+ }
254
+
255
+ function generateCDPCommands(replayData: ReplayData, speed: number): CDPCommand[] {
256
+ const events = [
257
+ ...replayData.interactions.map(i => ({ ...i, eventType: 'interaction' })),
258
+ ...replayData.navigations.map(n => ({ ...n, eventType: 'navigation' }))
259
+ ].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
260
+
261
+ const commands: CDPCommand[] = [];
262
+ const startTime = new Date(replayData.startTime).getTime();
263
+
264
+ for (const event of events) {
265
+ const eventTime = new Date(event.timestamp).getTime();
266
+ const delay = Math.max(0, (eventTime - startTime) / speed);
267
+
268
+ if (event.eventType === 'navigation') {
269
+ commands.push({
270
+ method: 'Page.navigate',
271
+ params: { url: event.url },
272
+ delay: delay,
273
+ description: `Navigate to ${event.url}`
274
+ });
275
+ } else if (event.eventType === 'interaction') {
276
+ if (event.type === 'CLICK' && event.x !== undefined && event.y !== undefined) {
277
+ // Mouse down
278
+ commands.push({
279
+ method: 'Input.dispatchMouseEvent',
280
+ params: {
281
+ type: 'mousePressed',
282
+ x: event.x,
283
+ y: event.y,
284
+ button: 'left',
285
+ clickCount: 1
286
+ },
287
+ delay: delay,
288
+ description: `Click at (${event.x}, ${event.y}) on ${event.target}`
289
+ });
290
+
291
+ // Mouse up (after small delay)
292
+ commands.push({
293
+ method: 'Input.dispatchMouseEvent',
294
+ params: {
295
+ type: 'mouseReleased',
296
+ x: event.x,
297
+ y: event.y,
298
+ button: 'left',
299
+ clickCount: 1
300
+ },
301
+ delay: 50, // 50ms between down and up
302
+ description: `Release click at (${event.x}, ${event.y})`
303
+ });
304
+ } else if (event.type === 'SCROLL' && event.x !== undefined && event.y !== undefined) {
305
+ commands.push({
306
+ method: 'Runtime.evaluate',
307
+ params: {
308
+ expression: `window.scrollTo({left: ${event.x}, top: ${event.y}, behavior: 'smooth'})`
309
+ },
310
+ delay: delay,
311
+ description: `Scroll to (${event.x}, ${event.y})`
312
+ });
313
+ } else if (event.type === 'KEY' && event.key) {
314
+ // Key down
315
+ commands.push({
316
+ method: 'Input.dispatchKeyEvent',
317
+ params: {
318
+ type: 'keyDown',
319
+ key: event.key,
320
+ text: event.key.length === 1 ? event.key : undefined
321
+ },
322
+ delay: delay,
323
+ description: `Key down: ${event.key}`
324
+ });
325
+
326
+ // Key up
327
+ commands.push({
328
+ method: 'Input.dispatchKeyEvent',
329
+ params: {
330
+ type: 'keyUp',
331
+ key: event.key
332
+ },
333
+ delay: 50,
334
+ description: `Key up: ${event.key}`
335
+ });
336
+ }
337
+ }
338
+ }
339
+
340
+ return commands;
341
+ }
342
+
343
+ async function executeCDPCommands(commands: CDPCommand[]): Promise<any> {
344
+ // For now, we'll try to connect to the CDP session
345
+ // This would require the browser to be launched with --remote-debugging-port
346
+ // or we'd need to get the CDP endpoint from the Playwright instance
347
+
348
+ // Since we can't easily access the existing Playwright browser from here,
349
+ // let's return the commands for the client to execute
350
+ throw new Error('Direct CDP execution not yet implemented - browser connection needed');
351
+ }