dev3000 0.0.10 → 0.0.12
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,184 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseLogEntries } from './LogsClient';
|
|
3
|
+
import { LogEntry } from '../../types';
|
|
4
|
+
|
|
5
|
+
describe('parseLogEntries', () => {
|
|
6
|
+
it('should parse single-line log entries correctly', () => {
|
|
7
|
+
const logContent = `[2025-09-03T03:57:39.357Z] [SERVER] myapp:dev: Cache hits: 5, misses: 2
|
|
8
|
+
[2025-09-03T03:57:40.123Z] [BROWSER] [CONSOLE LOG] Component rendered successfully`;
|
|
9
|
+
|
|
10
|
+
const entries = parseLogEntries(logContent);
|
|
11
|
+
|
|
12
|
+
expect(entries).toHaveLength(2);
|
|
13
|
+
|
|
14
|
+
expect(entries[0]).toEqual({
|
|
15
|
+
timestamp: '2025-09-03T03:57:39.357Z',
|
|
16
|
+
source: 'SERVER',
|
|
17
|
+
message: 'myapp:dev: Cache hits: 5, misses: 2',
|
|
18
|
+
original: '[2025-09-03T03:57:39.357Z] [SERVER] myapp:dev: Cache hits: 5, misses: 2'
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(entries[1]).toEqual({
|
|
22
|
+
timestamp: '2025-09-03T03:57:40.123Z',
|
|
23
|
+
source: 'BROWSER',
|
|
24
|
+
message: '[CONSOLE LOG] Component rendered successfully',
|
|
25
|
+
original: '[2025-09-03T03:57:40.123Z] [BROWSER] [CONSOLE LOG] Component rendered successfully'
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should group multi-line log entries by timestamp', () => {
|
|
30
|
+
const logContent = `[2025-09-03T03:57:39.614Z] [SERVER] myapp:dev: api_request[warn] Server error: {
|
|
31
|
+
myapp:dev: request: '/api/v1/users/search',
|
|
32
|
+
myapp:dev: reason: Error [AuthError]: Missing authentication token
|
|
33
|
+
myapp:dev: at validateAuth (../../packages/auth/src/middleware.ts:45:28)
|
|
34
|
+
myapp:dev: 43 | const token = req.headers.authorization;
|
|
35
|
+
myapp:dev: 44 | if (!token) {
|
|
36
|
+
myapp:dev: > 45 | throw new Error('Missing authentication token');
|
|
37
|
+
myapp:dev: | ^
|
|
38
|
+
myapp:dev: 46 | }
|
|
39
|
+
myapp:dev: 47 | return validateToken(token);
|
|
40
|
+
myapp:dev: 48 | }
|
|
41
|
+
myapp:dev: code: 'auth_required',
|
|
42
|
+
myapp:dev: status: 401,
|
|
43
|
+
myapp:dev: attributes: {
|
|
44
|
+
myapp:dev: 'http.method': 'GET',
|
|
45
|
+
myapp:dev: 'http.url': 'http://localhost:3000/api/v1/users/search',
|
|
46
|
+
myapp:dev: 'http.status_code': 401,
|
|
47
|
+
myapp:dev: 'request.id': 'req_abc123def456'
|
|
48
|
+
myapp:dev: },
|
|
49
|
+
myapp:dev: missingAuth: true
|
|
50
|
+
myapp:dev: }
|
|
51
|
+
myapp:dev: }
|
|
52
|
+
[2025-09-03T03:57:40.778Z] [SERVER] myapp:dev: Cache[info] Prefetch unused: [ '"/api/v2/users?page=1"' ]`;
|
|
53
|
+
|
|
54
|
+
const entries = parseLogEntries(logContent);
|
|
55
|
+
|
|
56
|
+
expect(entries).toHaveLength(2);
|
|
57
|
+
|
|
58
|
+
// First entry should contain the entire multi-line error
|
|
59
|
+
expect(entries[0].timestamp).toBe('2025-09-03T03:57:39.614Z');
|
|
60
|
+
expect(entries[0].source).toBe('SERVER');
|
|
61
|
+
expect(entries[0].message).toContain('api_request[warn] Server error: {');
|
|
62
|
+
expect(entries[0].message).toContain('Missing authentication token');
|
|
63
|
+
expect(entries[0].message).toContain('missingAuth: true');
|
|
64
|
+
expect(entries[0].message).toContain('}');
|
|
65
|
+
|
|
66
|
+
// Second entry should be the single-line cache message
|
|
67
|
+
expect(entries[1].timestamp).toBe('2025-09-03T03:57:40.778Z');
|
|
68
|
+
expect(entries[1].source).toBe('SERVER');
|
|
69
|
+
expect(entries[1].message).toBe('myapp:dev: Cache[info] Prefetch unused: [ \'"/api/v2/users?page=1"\' ]');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should handle screenshot entries correctly', () => {
|
|
73
|
+
const logContent = `[2025-09-03T02:14:27.444Z] [BROWSER] [SCREENSHOT] http://localhost:3684/screenshots/2025-09-03T02-14-27-329Z-initial-load.png
|
|
74
|
+
[2025-09-03T02:14:27.444Z] [BROWSER] 📄 New page: http://localhost:3000/`;
|
|
75
|
+
|
|
76
|
+
const entries = parseLogEntries(logContent);
|
|
77
|
+
|
|
78
|
+
expect(entries).toHaveLength(2);
|
|
79
|
+
|
|
80
|
+
expect(entries[0].screenshot).toBe('http://localhost:3684/screenshots/2025-09-03T02-14-27-329Z-initial-load.png');
|
|
81
|
+
expect(entries[0].message).toBe('[SCREENSHOT] http://localhost:3684/screenshots/2025-09-03T02-14-27-329Z-initial-load.png');
|
|
82
|
+
|
|
83
|
+
expect(entries[1].screenshot).toBeUndefined();
|
|
84
|
+
expect(entries[1].message).toBe('📄 New page: http://localhost:3000/');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should handle complex stack traces and nested objects', () => {
|
|
88
|
+
const logContent = `[2025-09-03T03:57:39.098Z] [SERVER] ❌ Database connection failed: DatabaseError: Connection timeout
|
|
89
|
+
myapp:dev: at ConnectionPool.connect (/app/src/database/pool.ts:142:15)
|
|
90
|
+
myapp:dev: at DatabaseService.initialize (/app/src/services/database.ts:87:31)
|
|
91
|
+
myapp:dev: at async Server.start (/app/src/server.ts:56:8)
|
|
92
|
+
myapp:dev: > 142 | throw new DatabaseError('Connection timeout');
|
|
93
|
+
myapp:dev: | ^
|
|
94
|
+
myapp:dev: 143 | }
|
|
95
|
+
myapp:dev: 144 | return connection;
|
|
96
|
+
myapp:dev: 145 | }
|
|
97
|
+
[2025-09-03T03:57:39.098Z] [SERVER] ❌ Error details: {
|
|
98
|
+
myapp:dev: message: 'connection.query is not a function',
|
|
99
|
+
myapp:dev: stack: 'TypeError: connection.query is not a function\\n' +
|
|
100
|
+
myapp:dev: ' at Database.execute (/app/src/database/client.ts:89:27)\\n' +
|
|
101
|
+
myapp:dev: ' at UserService.findById (/app/src/services/user.ts:45:19)\\n' +
|
|
102
|
+
myapp:dev: ' at async Handler.getUser (/app/src/handlers/user.ts:23:18)',
|
|
103
|
+
myapp:dev: config: {
|
|
104
|
+
myapp:dev: host: 'localhost',
|
|
105
|
+
myapp:dev: port: 5432,
|
|
106
|
+
myapp:dev: database: 'myapp_dev',
|
|
107
|
+
myapp:dev: retries: 3
|
|
108
|
+
myapp:dev: },
|
|
109
|
+
myapp:dev: connectionId: 'conn_xyz789abc'
|
|
110
|
+
myapp:dev: }
|
|
111
|
+
[2025-09-03T03:57:40.200Z] [BROWSER] [CONSOLE ERROR] Uncaught TypeError: Cannot read properties of null`;
|
|
112
|
+
|
|
113
|
+
const entries = parseLogEntries(logContent);
|
|
114
|
+
|
|
115
|
+
expect(entries).toHaveLength(3);
|
|
116
|
+
|
|
117
|
+
// First entry - connection error with stack trace
|
|
118
|
+
expect(entries[0].timestamp).toBe('2025-09-03T03:57:39.098Z');
|
|
119
|
+
expect(entries[0].source).toBe('SERVER');
|
|
120
|
+
expect(entries[0].message).toContain('❌ Database connection failed');
|
|
121
|
+
expect(entries[0].message).toContain('ConnectionPool.connect');
|
|
122
|
+
expect(entries[0].message).toContain('Connection timeout');
|
|
123
|
+
|
|
124
|
+
// Second entry - error details object
|
|
125
|
+
expect(entries[1].timestamp).toBe('2025-09-03T03:57:39.098Z');
|
|
126
|
+
expect(entries[1].source).toBe('SERVER');
|
|
127
|
+
expect(entries[1].message).toContain('❌ Error details: {');
|
|
128
|
+
expect(entries[1].message).toContain('connection.query is not a function');
|
|
129
|
+
expect(entries[1].message).toContain('connectionId: \'conn_xyz789abc\'');
|
|
130
|
+
|
|
131
|
+
// Third entry - browser error
|
|
132
|
+
expect(entries[2].timestamp).toBe('2025-09-03T03:57:40.200Z');
|
|
133
|
+
expect(entries[2].source).toBe('BROWSER');
|
|
134
|
+
expect(entries[2].message).toBe('[CONSOLE ERROR] Uncaught TypeError: Cannot read properties of null');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle empty and malformed log content', () => {
|
|
138
|
+
expect(parseLogEntries('')).toHaveLength(0);
|
|
139
|
+
expect(parseLogEntries(' \n \n ')).toHaveLength(0);
|
|
140
|
+
expect(parseLogEntries('Invalid log line without timestamp')).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should preserve original content for debugging', () => {
|
|
144
|
+
const logContent = `[2025-09-03T03:57:39.357Z] [SERVER] API response: {
|
|
145
|
+
myapp:dev: userId: 'user_123',
|
|
146
|
+
myapp:dev: status: 'active'
|
|
147
|
+
myapp:dev: }`;
|
|
148
|
+
|
|
149
|
+
const entries = parseLogEntries(logContent);
|
|
150
|
+
|
|
151
|
+
expect(entries).toHaveLength(1);
|
|
152
|
+
expect(entries[0].original).toBe(logContent);
|
|
153
|
+
expect(entries[0].message).toContain('API response: {');
|
|
154
|
+
expect(entries[0].message).toContain('userId: \'user_123\'');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should handle mixed single-line and multi-line entries', () => {
|
|
158
|
+
const logContent = `[2025-09-03T03:57:38.500Z] [SERVER] ✓ Server started on port 3000
|
|
159
|
+
[2025-09-03T03:57:39.614Z] [SERVER] myapp:dev: validation[error] Request failed: {
|
|
160
|
+
myapp:dev: endpoint: '/api/v1/validate',
|
|
161
|
+
myapp:dev: errors: [
|
|
162
|
+
myapp:dev: { field: 'email', message: 'Invalid format' },
|
|
163
|
+
myapp:dev: { field: 'password', message: 'Too short' }
|
|
164
|
+
myapp:dev: ],
|
|
165
|
+
myapp:dev: requestId: 'req_validation_456'
|
|
166
|
+
myapp:dev: }
|
|
167
|
+
[2025-09-03T03:57:40.100Z] [BROWSER] [NAVIGATION] http://localhost:3000/dashboard
|
|
168
|
+
[2025-09-03T03:57:40.300Z] [SERVER] Dashboard page rendered in 89ms`;
|
|
169
|
+
|
|
170
|
+
const entries = parseLogEntries(logContent);
|
|
171
|
+
|
|
172
|
+
expect(entries).toHaveLength(4);
|
|
173
|
+
|
|
174
|
+
// Single-line entries
|
|
175
|
+
expect(entries[0].message).toBe('✓ Server started on port 3000');
|
|
176
|
+
expect(entries[2].message).toBe('[NAVIGATION] http://localhost:3000/dashboard');
|
|
177
|
+
expect(entries[3].message).toBe('Dashboard page rendered in 89ms');
|
|
178
|
+
|
|
179
|
+
// Multi-line entry
|
|
180
|
+
expect(entries[1].message).toContain('validation[error] Request failed: {');
|
|
181
|
+
expect(entries[1].message).toContain('Invalid format');
|
|
182
|
+
expect(entries[1].message).toContain('requestId: \'req_validation_456\'');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -3,8 +3,53 @@
|
|
|
3
3
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
4
4
|
import { LogEntry, LogsApiResponse, ConfigApiResponse } from '../../types';
|
|
5
5
|
|
|
6
|
+
export function parseLogEntries(logContent: string): LogEntry[] {
|
|
7
|
+
// Split by timestamp pattern - each timestamp starts a new log entry
|
|
8
|
+
const timestampPattern = /\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)\] \[([^\]]+)\] /;
|
|
9
|
+
|
|
10
|
+
const entries: LogEntry[] = [];
|
|
11
|
+
const lines = logContent.split('\n');
|
|
12
|
+
let currentEntry: LogEntry | null = null;
|
|
13
|
+
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
if (!line.trim()) continue;
|
|
16
|
+
|
|
17
|
+
const match = line.match(timestampPattern);
|
|
18
|
+
if (match) {
|
|
19
|
+
// Save previous entry if exists
|
|
20
|
+
if (currentEntry) {
|
|
21
|
+
entries.push(currentEntry);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Start new entry
|
|
25
|
+
const [fullMatch, timestamp, source] = match;
|
|
26
|
+
const message = line.substring(fullMatch.length);
|
|
27
|
+
const screenshot = message.match(/\[SCREENSHOT\] (.+)/)?.[1];
|
|
28
|
+
|
|
29
|
+
currentEntry = {
|
|
30
|
+
timestamp,
|
|
31
|
+
source,
|
|
32
|
+
message,
|
|
33
|
+
screenshot,
|
|
34
|
+
original: line
|
|
35
|
+
};
|
|
36
|
+
} else if (currentEntry) {
|
|
37
|
+
// Append to current entry's message
|
|
38
|
+
currentEntry.message += '\n' + line;
|
|
39
|
+
currentEntry.original += '\n' + line;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Don't forget the last entry
|
|
44
|
+
if (currentEntry) {
|
|
45
|
+
entries.push(currentEntry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return entries;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Keep this for backwards compatibility, but it's not used anymore
|
|
6
52
|
function parseLogLine(line: string): LogEntry | null {
|
|
7
|
-
// More robust parsing - match timestamp and source, then take everything else as message
|
|
8
53
|
const match = line.match(/^\[([^\]]+)\] \[([^\]]+)\] (.*)$/s);
|
|
9
54
|
if (!match) return null;
|
|
10
55
|
|
|
@@ -81,11 +126,7 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
81
126
|
return;
|
|
82
127
|
}
|
|
83
128
|
|
|
84
|
-
const entries = data.logs
|
|
85
|
-
.split('\n')
|
|
86
|
-
.filter((line: string) => line.trim())
|
|
87
|
-
.map(parseLogLine)
|
|
88
|
-
.filter((entry: LogEntry | null): entry is LogEntry => entry !== null);
|
|
129
|
+
const entries = parseLogEntries(data.logs);
|
|
89
130
|
|
|
90
131
|
if (entries.length > lastLogCount) {
|
|
91
132
|
setIsLoadingNew(true);
|
|
@@ -137,11 +178,7 @@ export default function LogsClient({ version }: LogsClientProps) {
|
|
|
137
178
|
return;
|
|
138
179
|
}
|
|
139
180
|
|
|
140
|
-
const entries = data.logs
|
|
141
|
-
.split('\n')
|
|
142
|
-
.filter((line: string) => line.trim())
|
|
143
|
-
.map(parseLogLine)
|
|
144
|
-
.filter((entry: LogEntry | null): entry is LogEntry => entry !== null);
|
|
181
|
+
const entries = parseLogEntries(data.logs);
|
|
145
182
|
|
|
146
183
|
setLogs(entries);
|
|
147
184
|
setLastLogCount(entries.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev3000",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "AI-powered development tools with browser monitoring and MCP server integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,7 +38,9 @@
|
|
|
38
38
|
"tsx": "^4.20.3",
|
|
39
39
|
"@types/node": "^22.8.7",
|
|
40
40
|
"@types/cli-progress": "^3.11.6",
|
|
41
|
-
"typescript": "^5.0.0"
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"vitest": "^1.0.0",
|
|
43
|
+
"husky": "^9.0.0"
|
|
42
44
|
},
|
|
43
45
|
"engines": {
|
|
44
46
|
"node": ">=18.0.0"
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
"scripts": {
|
|
53
55
|
"build": "tsc",
|
|
54
56
|
"dev": "tsc --watch",
|
|
55
|
-
"
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"release": "pnpm run build && pnpm test && pnpm version patch && git push origin main --tags && pnpm publish --otp=$(op item get npm --otp)"
|
|
56
59
|
}
|
|
57
60
|
}
|