dev3000 0.0.24 → 0.0.30
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 +15 -5
- package/dist/cdp-monitor.d.ts +39 -0
- package/dist/cdp-monitor.d.ts.map +1 -0
- package/dist/cdp-monitor.js +697 -0
- package/dist/cdp-monitor.js.map +1 -0
- package/dist/cli.js +13 -3
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +4 -17
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +75 -573
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/logs/rotate/route.ts +63 -0
- package/mcp-server/app/api/mcp/[transport]/route.ts +221 -0
- package/mcp-server/app/api/replay/route.ts +70 -9
- package/mcp-server/app/logs/LogsClient.tsx +56 -6
- package/mcp-server/app/page.tsx +1 -1
- package/mcp-server/package.json +3 -1
- package/mcp-server/pnpm-lock.yaml +1491 -0
- package/package.json +10 -5
package/dist/dev-environment.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { chromium } from 'playwright';
|
|
3
2
|
import { writeFileSync, appendFileSync, mkdirSync, existsSync, copyFileSync, readFileSync, cpSync, lstatSync, symlinkSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
4
3
|
import { join, dirname, basename } from 'path';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { tmpdir } from 'os';
|
|
7
6
|
import chalk from 'chalk';
|
|
8
7
|
import * as cliProgress from 'cli-progress';
|
|
8
|
+
import { CDPMonitor } from './cdp-monitor.js';
|
|
9
9
|
class Logger {
|
|
10
10
|
logFile;
|
|
11
11
|
constructor(logFile) {
|
|
@@ -24,15 +24,6 @@ class Logger {
|
|
|
24
24
|
appendFileSync(this.logFile, logEntry);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
function detectPackageManager() {
|
|
28
|
-
if (existsSync('pnpm-lock.yaml'))
|
|
29
|
-
return 'pnpx';
|
|
30
|
-
if (existsSync('yarn.lock'))
|
|
31
|
-
return 'yarn dlx';
|
|
32
|
-
if (existsSync('package-lock.json'))
|
|
33
|
-
return 'npx';
|
|
34
|
-
return 'npx'; // fallback
|
|
35
|
-
}
|
|
36
27
|
function detectPackageManagerForRun() {
|
|
37
28
|
if (existsSync('pnpm-lock.yaml'))
|
|
38
29
|
return 'pnpm';
|
|
@@ -116,11 +107,8 @@ function pruneOldLogs(baseDir, cwdName) {
|
|
|
116
107
|
export class DevEnvironment {
|
|
117
108
|
serverProcess = null;
|
|
118
109
|
mcpServerProcess = null;
|
|
119
|
-
|
|
120
|
-
browserContext = null;
|
|
110
|
+
cdpMonitor = null;
|
|
121
111
|
logger;
|
|
122
|
-
stateTimer = null;
|
|
123
|
-
browserType = null;
|
|
124
112
|
options;
|
|
125
113
|
screenshotDir;
|
|
126
114
|
mcpPublicDir;
|
|
@@ -144,9 +132,19 @@ export class DevEnvironment {
|
|
|
144
132
|
const packageJsonPath = join(packageRoot, 'package.json');
|
|
145
133
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
146
134
|
this.version = packageJson.version;
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
135
|
+
// Use git to detect if we're in the dev3000 source repository
|
|
136
|
+
try {
|
|
137
|
+
const { execSync } = require('child_process');
|
|
138
|
+
const gitRemote = execSync('git remote get-url origin 2>/dev/null', {
|
|
139
|
+
cwd: packageRoot,
|
|
140
|
+
encoding: 'utf8'
|
|
141
|
+
}).trim();
|
|
142
|
+
if (gitRemote.includes('vercel-labs/dev3000') && !this.version.includes('canary')) {
|
|
143
|
+
this.version += '-local';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Not in git repo or no git - use version as-is
|
|
150
148
|
}
|
|
151
149
|
}
|
|
152
150
|
catch (error) {
|
|
@@ -168,11 +166,6 @@ export class DevEnvironment {
|
|
|
168
166
|
mkdirSync(this.mcpPublicDir, { recursive: true });
|
|
169
167
|
}
|
|
170
168
|
}
|
|
171
|
-
debugLog(message) {
|
|
172
|
-
if (this.options.debug) {
|
|
173
|
-
console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
169
|
async checkPortsAvailable() {
|
|
177
170
|
const ports = [this.options.port, this.options.mcpPort];
|
|
178
171
|
for (const port of ports) {
|
|
@@ -221,8 +214,8 @@ export class DevEnvironment {
|
|
|
221
214
|
this.progressBar.update(60, { stage: 'Waiting for MCP server...' });
|
|
222
215
|
await this.waitForMcpServer();
|
|
223
216
|
this.progressBar.update(80, { stage: 'Starting browser...' });
|
|
224
|
-
// Start
|
|
225
|
-
this.
|
|
217
|
+
// Start CDP monitoring but don't wait for full setup
|
|
218
|
+
this.startCDPMonitoringAsync();
|
|
226
219
|
this.progressBar.update(100, { stage: 'Complete!' });
|
|
227
220
|
// Stop progress bar and show results immediately
|
|
228
221
|
this.progressBar.stop();
|
|
@@ -231,7 +224,7 @@ export class DevEnvironment {
|
|
|
231
224
|
console.log(chalk.blue(`Logs symlink: /tmp/dev3000.log`));
|
|
232
225
|
console.log(chalk.yellow('☝️ Give this to an AI to auto debug and fix your app\n'));
|
|
233
226
|
console.log(chalk.blue(`🌐 Your App: http://localhost:${this.options.port}`));
|
|
234
|
-
console.log(chalk.blue(`🤖 MCP Server: http://localhost:${this.options.mcpPort}/api/mcp/
|
|
227
|
+
console.log(chalk.blue(`🤖 MCP Server: http://localhost:${this.options.mcpPort}/api/mcp/mcp`));
|
|
235
228
|
console.log(chalk.magenta(`📸 Visual Timeline: http://localhost:${this.options.mcpPort}/logs`));
|
|
236
229
|
console.log(chalk.gray('\n💡 To stop all servers and kill dev3000: Ctrl-C'));
|
|
237
230
|
}
|
|
@@ -270,18 +263,28 @@ export class DevEnvironment {
|
|
|
270
263
|
console.log(chalk.red(`Server process exited with code ${code}`));
|
|
271
264
|
});
|
|
272
265
|
}
|
|
266
|
+
debugLog(message) {
|
|
267
|
+
if (this.options.debug) {
|
|
268
|
+
console.log(`[MCP DEBUG] ${message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
273
271
|
async startMcpServer() {
|
|
272
|
+
this.debugLog('Starting MCP server setup');
|
|
274
273
|
// Get the path to our bundled MCP server
|
|
275
274
|
const currentFile = fileURLToPath(import.meta.url);
|
|
276
275
|
const packageRoot = dirname(dirname(currentFile)); // Go up from dist/ to package root
|
|
277
276
|
const mcpServerPath = join(packageRoot, 'mcp-server');
|
|
277
|
+
this.debugLog(`MCP server path: ${mcpServerPath}`);
|
|
278
278
|
if (!existsSync(mcpServerPath)) {
|
|
279
279
|
throw new Error(`MCP server directory not found at ${mcpServerPath}`);
|
|
280
280
|
}
|
|
281
|
+
this.debugLog('MCP server directory found');
|
|
281
282
|
// Check if MCP server dependencies are installed, install if missing
|
|
282
283
|
const isGlobalInstall = mcpServerPath.includes('.pnpm');
|
|
284
|
+
this.debugLog(`Is global install: ${isGlobalInstall}`);
|
|
283
285
|
let nodeModulesPath = join(mcpServerPath, 'node_modules');
|
|
284
286
|
let actualWorkingDir = mcpServerPath;
|
|
287
|
+
this.debugLog(`Node modules path: ${nodeModulesPath}`);
|
|
285
288
|
if (isGlobalInstall) {
|
|
286
289
|
const tmpDirPath = join(tmpdir(), 'dev3000-mcp-deps');
|
|
287
290
|
nodeModulesPath = join(tmpDirPath, 'node_modules');
|
|
@@ -292,15 +295,13 @@ export class DevEnvironment {
|
|
|
292
295
|
mkdirSync(this.screenshotDir, { recursive: true });
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
this.progressBar.start(100, 20, { stage: 'Starting MCP server...' });
|
|
303
|
-
}
|
|
298
|
+
// Always install dependencies to ensure they're up to date
|
|
299
|
+
this.debugLog('Installing/updating MCP server dependencies');
|
|
300
|
+
this.progressBar.stop();
|
|
301
|
+
console.log(chalk.blue('\n📦 Installing MCP server dependencies...'));
|
|
302
|
+
await this.installMcpServerDeps(mcpServerPath);
|
|
303
|
+
console.log(''); // Add spacing
|
|
304
|
+
this.progressBar.start(100, 20, { stage: 'Starting MCP server...' });
|
|
304
305
|
// Use version already read in constructor
|
|
305
306
|
// For global installs, ensure all necessary files are copied to temp directory
|
|
306
307
|
if (isGlobalInstall && actualWorkingDir !== mcpServerPath) {
|
|
@@ -343,6 +344,9 @@ export class DevEnvironment {
|
|
|
343
344
|
}
|
|
344
345
|
// Start the MCP server using detected package manager
|
|
345
346
|
const packageManagerForRun = detectPackageManagerForRun();
|
|
347
|
+
this.debugLog(`Using package manager: ${packageManagerForRun}`);
|
|
348
|
+
this.debugLog(`MCP server working directory: ${actualWorkingDir}`);
|
|
349
|
+
this.debugLog(`MCP server port: ${this.options.mcpPort}`);
|
|
346
350
|
this.mcpServerProcess = spawn(packageManagerForRun, ['run', 'dev'], {
|
|
347
351
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
348
352
|
shell: true,
|
|
@@ -355,6 +359,7 @@ export class DevEnvironment {
|
|
|
355
359
|
DEV3000_VERSION: this.version, // Pass version to MCP server
|
|
356
360
|
},
|
|
357
361
|
});
|
|
362
|
+
this.debugLog('MCP server process spawned');
|
|
358
363
|
// Log MCP server output to separate file for debugging
|
|
359
364
|
const mcpLogFile = join(dirname(this.options.logFile), 'dev3000-mcp.log');
|
|
360
365
|
writeFileSync(mcpLogFile, ''); // Clear the file
|
|
@@ -377,136 +382,13 @@ export class DevEnvironment {
|
|
|
377
382
|
}
|
|
378
383
|
});
|
|
379
384
|
this.mcpServerProcess.on('exit', (code) => {
|
|
385
|
+
this.debugLog(`MCP server process exited with code ${code}`);
|
|
380
386
|
// Only show exit messages for unexpected failures, not restarts
|
|
381
387
|
if (code !== 0 && code !== null) {
|
|
382
388
|
this.logger.log('server', `MCP server process exited with code ${code}`);
|
|
383
389
|
}
|
|
384
390
|
});
|
|
385
|
-
|
|
386
|
-
startStateSaving() {
|
|
387
|
-
if (this.stateTimer) {
|
|
388
|
-
clearInterval(this.stateTimer);
|
|
389
|
-
}
|
|
390
|
-
// Start continuous autosave timer (no focus dependency since it's non-intrusive)
|
|
391
|
-
this.stateTimer = setInterval(async () => {
|
|
392
|
-
if (this.browserContext) {
|
|
393
|
-
try {
|
|
394
|
-
this.debugLog('Running periodic context autosave (non-intrusive)...');
|
|
395
|
-
await this.saveStateManually();
|
|
396
|
-
this.debugLog('Context autosave completed successfully');
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
400
|
-
this.debugLog(`Context autosave failed: ${errorMessage}`);
|
|
401
|
-
// If context is closed, stop the timer
|
|
402
|
-
if (errorMessage.includes('closed') || errorMessage.includes('destroyed')) {
|
|
403
|
-
this.debugLog('Browser context appears closed, stopping autosave timer');
|
|
404
|
-
this.stopStateSaving();
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}, 15000); // Save every 15 seconds
|
|
409
|
-
}
|
|
410
|
-
stopStateSaving() {
|
|
411
|
-
if (this.stateTimer) {
|
|
412
|
-
clearInterval(this.stateTimer);
|
|
413
|
-
this.stateTimer = null;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
async saveStateManually() {
|
|
417
|
-
if (!this.browserContext)
|
|
418
|
-
return;
|
|
419
|
-
const stateDir = this.options.profileDir;
|
|
420
|
-
const cookiesFile = join(stateDir, 'cookies.json');
|
|
421
|
-
const storageFile = join(stateDir, 'storage.json');
|
|
422
|
-
try {
|
|
423
|
-
// Save cookies (non-intrusive)
|
|
424
|
-
const cookies = await this.browserContext.cookies();
|
|
425
|
-
writeFileSync(cookiesFile, JSON.stringify(cookies, null, 2));
|
|
426
|
-
// Save localStorage and sessionStorage from current pages (non-intrusive)
|
|
427
|
-
const pages = this.browserContext.pages();
|
|
428
|
-
if (pages.length > 0) {
|
|
429
|
-
const page = pages[0]; // Use first page to avoid creating new ones
|
|
430
|
-
if (!page.isClosed()) {
|
|
431
|
-
const storageData = await page.evaluate(() => {
|
|
432
|
-
return {
|
|
433
|
-
localStorage: JSON.stringify(localStorage),
|
|
434
|
-
sessionStorage: JSON.stringify(sessionStorage),
|
|
435
|
-
url: window.location.href
|
|
436
|
-
};
|
|
437
|
-
});
|
|
438
|
-
writeFileSync(storageFile, JSON.stringify(storageData, null, 2));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
// Re-throw to be handled by caller
|
|
444
|
-
throw error;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
async loadStateManually() {
|
|
448
|
-
if (!this.browserContext)
|
|
449
|
-
return;
|
|
450
|
-
const stateDir = this.options.profileDir;
|
|
451
|
-
const cookiesFile = join(stateDir, 'cookies.json');
|
|
452
|
-
const storageFile = join(stateDir, 'storage.json');
|
|
453
|
-
try {
|
|
454
|
-
// Load cookies if they exist
|
|
455
|
-
if (existsSync(cookiesFile)) {
|
|
456
|
-
const cookies = JSON.parse(readFileSync(cookiesFile, 'utf8'));
|
|
457
|
-
if (Array.isArray(cookies) && cookies.length > 0) {
|
|
458
|
-
await this.browserContext.addCookies(cookies);
|
|
459
|
-
this.debugLog(`Restored ${cookies.length} cookies`);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
// Load storage data for later restoration (we'll apply it after page navigation)
|
|
463
|
-
if (existsSync(storageFile)) {
|
|
464
|
-
const storageData = JSON.parse(readFileSync(storageFile, 'utf8'));
|
|
465
|
-
if (storageData.localStorage || storageData.sessionStorage) {
|
|
466
|
-
// Store this for restoration after page loads
|
|
467
|
-
this.browserContext._dev3000_storageData = storageData;
|
|
468
|
-
this.debugLog('Loaded storage data for restoration');
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
this.debugLog(`Failed to load saved state: ${error instanceof Error ? error.message : String(error)}`);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
async restoreStorageData(page) {
|
|
477
|
-
if (!this.browserContext || page.isClosed())
|
|
478
|
-
return;
|
|
479
|
-
const storageData = this.browserContext._dev3000_storageData;
|
|
480
|
-
if (!storageData)
|
|
481
|
-
return;
|
|
482
|
-
try {
|
|
483
|
-
await page.evaluate((data) => {
|
|
484
|
-
// Restore localStorage
|
|
485
|
-
if (data.localStorage) {
|
|
486
|
-
const localStorageData = JSON.parse(data.localStorage);
|
|
487
|
-
Object.keys(localStorageData).forEach(key => {
|
|
488
|
-
localStorage.setItem(key, localStorageData[key]);
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
// Restore sessionStorage
|
|
492
|
-
if (data.sessionStorage) {
|
|
493
|
-
const sessionStorageData = JSON.parse(data.sessionStorage);
|
|
494
|
-
Object.keys(sessionStorageData).forEach(key => {
|
|
495
|
-
sessionStorage.setItem(key, sessionStorageData[key]);
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}, storageData);
|
|
499
|
-
this.debugLog('Restored localStorage and sessionStorage');
|
|
500
|
-
// Clear the stored data since it's been applied
|
|
501
|
-
delete this.browserContext._dev3000_storageData;
|
|
502
|
-
}
|
|
503
|
-
catch (error) {
|
|
504
|
-
this.debugLog(`Failed to restore storage data: ${error instanceof Error ? error.message : String(error)}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async setupFocusHandlers(page) {
|
|
508
|
-
// Note: Focus handlers removed since autosave is now continuous and non-intrusive
|
|
509
|
-
// The autosave timer will detect context closure through its own error handling
|
|
391
|
+
this.debugLog('MCP server event handlers setup complete');
|
|
510
392
|
}
|
|
511
393
|
async waitForServer() {
|
|
512
394
|
const maxAttempts = 30;
|
|
@@ -625,409 +507,56 @@ export class DevEnvironment {
|
|
|
625
507
|
let attempts = 0;
|
|
626
508
|
while (attempts < maxAttempts) {
|
|
627
509
|
try {
|
|
510
|
+
// Test the actual MCP endpoint
|
|
628
511
|
const response = await fetch(`http://localhost:${this.options.mcpPort}`, {
|
|
629
512
|
method: 'HEAD',
|
|
630
513
|
signal: AbortSignal.timeout(2000)
|
|
631
514
|
});
|
|
632
|
-
|
|
515
|
+
this.debugLog(`MCP server health check: ${response.status}`);
|
|
516
|
+
if (response.status === 500) {
|
|
517
|
+
const errorText = await response.text();
|
|
518
|
+
this.debugLog(`MCP server 500 error: ${errorText}`);
|
|
519
|
+
}
|
|
520
|
+
if (response.ok || response.status === 404) { // 404 is OK - means server is responding
|
|
633
521
|
return;
|
|
634
522
|
}
|
|
635
523
|
}
|
|
636
524
|
catch (error) {
|
|
637
|
-
|
|
525
|
+
this.debugLog(`MCP server not ready (attempt ${attempts}): ${error}`);
|
|
638
526
|
}
|
|
639
527
|
attempts++;
|
|
640
528
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
641
529
|
}
|
|
642
|
-
|
|
530
|
+
this.debugLog('MCP server health check failed, terminating');
|
|
531
|
+
throw new Error(`MCP server failed to start after ${maxAttempts} seconds. Check the logs for errors.`);
|
|
643
532
|
}
|
|
644
|
-
|
|
645
|
-
// Start
|
|
646
|
-
this.
|
|
647
|
-
console.error(chalk.red('⚠️
|
|
533
|
+
startCDPMonitoringAsync() {
|
|
534
|
+
// Start CDP monitoring in background without blocking completion
|
|
535
|
+
this.startCDPMonitoring().catch(error => {
|
|
536
|
+
console.error(chalk.red('⚠️ CDP monitoring setup failed:'), error);
|
|
648
537
|
});
|
|
649
538
|
}
|
|
650
|
-
async
|
|
539
|
+
async startCDPMonitoring() {
|
|
651
540
|
// Ensure profile directory exists
|
|
652
541
|
if (!existsSync(this.options.profileDir)) {
|
|
653
542
|
mkdirSync(this.options.profileDir, { recursive: true });
|
|
654
543
|
}
|
|
544
|
+
// Initialize CDP monitor with enhanced logging
|
|
545
|
+
this.cdpMonitor = new CDPMonitor(this.options.profileDir, (source, message) => {
|
|
546
|
+
this.logger.log('browser', message);
|
|
547
|
+
}, this.options.debug);
|
|
655
548
|
try {
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
'--disable-web-security', // Keep this for dev server access
|
|
663
|
-
'--hide-crash-restore-bubble', // Don't ask to restore pages
|
|
664
|
-
'--disable-infobars', // Remove info bars
|
|
665
|
-
'--disable-blink-features=AutomationControlled', // Hide automation detection
|
|
666
|
-
'--disable-features=VizDisplayCompositor', // Reduce automation fingerprinting
|
|
667
|
-
],
|
|
668
|
-
});
|
|
669
|
-
this.browserType = 'system-chrome';
|
|
670
|
-
}
|
|
671
|
-
catch (error) {
|
|
672
|
-
// Fallback to Playwright's bundled chromium
|
|
673
|
-
try {
|
|
674
|
-
this.browser = await chromium.launch({
|
|
675
|
-
headless: false,
|
|
676
|
-
// Remove automation flags to allow normal dialog behavior
|
|
677
|
-
args: [
|
|
678
|
-
'--disable-web-security', // Keep this for dev server access
|
|
679
|
-
'--hide-crash-restore-bubble', // Don't ask to restore pages
|
|
680
|
-
'--disable-infobars', // Remove info bars
|
|
681
|
-
'--disable-blink-features=AutomationControlled', // Hide automation detection
|
|
682
|
-
'--disable-features=VizDisplayCompositor', // Reduce automation fingerprinting
|
|
683
|
-
],
|
|
684
|
-
});
|
|
685
|
-
this.browserType = 'playwright-chromium';
|
|
686
|
-
}
|
|
687
|
-
catch (playwrightError) {
|
|
688
|
-
if (playwrightError.message?.includes('Executable doesn\'t exist')) {
|
|
689
|
-
detectPackageManager();
|
|
690
|
-
console.log(chalk.yellow('📦 Installing Playwright chromium browser...'));
|
|
691
|
-
await this.installPlaywrightBrowsers();
|
|
692
|
-
// Retry with bundled chromium
|
|
693
|
-
this.browser = await chromium.launch({
|
|
694
|
-
headless: false,
|
|
695
|
-
// Remove automation flags to allow normal dialog behavior
|
|
696
|
-
args: [
|
|
697
|
-
'--disable-web-security', // Keep this for dev server access
|
|
698
|
-
'--hide-crash-restore-bubble', // Don't ask to restore pages
|
|
699
|
-
'--disable-infobars', // Remove info bars
|
|
700
|
-
],
|
|
701
|
-
});
|
|
702
|
-
this.browserType = 'playwright-chromium';
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
throw playwrightError;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
// Create context with viewport: null to enable window resizing
|
|
710
|
-
this.browserContext = await this.browser.newContext({
|
|
711
|
-
viewport: null, // This makes the page size depend on the window size
|
|
712
|
-
});
|
|
713
|
-
// Restore state manually (non-intrusive)
|
|
714
|
-
await this.loadStateManually();
|
|
715
|
-
// Set up focus-aware periodic storage state saving
|
|
716
|
-
this.startStateSaving();
|
|
717
|
-
// Navigate to the app using the existing blank page
|
|
718
|
-
const pages = this.browserContext.pages();
|
|
719
|
-
const page = pages.length > 0 ? pages[0] : await this.browserContext.newPage();
|
|
720
|
-
// Disable automatic dialog handling - let dialogs behave naturally
|
|
721
|
-
page.removeAllListeners('dialog');
|
|
722
|
-
// Add a no-op dialog handler to prevent auto-dismissal
|
|
723
|
-
page.on('dialog', async (dialog) => {
|
|
724
|
-
// Don't accept or dismiss - let user handle it manually
|
|
725
|
-
// This prevents Playwright from auto-handling the dialog
|
|
726
|
-
});
|
|
727
|
-
await page.goto(`http://localhost:${this.options.port}`);
|
|
728
|
-
// Restore localStorage and sessionStorage after navigation
|
|
729
|
-
await this.restoreStorageData(page);
|
|
730
|
-
// Set up focus detection after navigation to prevent context execution errors
|
|
731
|
-
await this.setupFocusHandlers(page);
|
|
732
|
-
// Take initial screenshot
|
|
733
|
-
const initialScreenshot = await this.takeScreenshot(page, 'initial-load');
|
|
734
|
-
if (initialScreenshot) {
|
|
735
|
-
this.logger.log('browser', `[SCREENSHOT] ${initialScreenshot}`);
|
|
736
|
-
}
|
|
737
|
-
// Set up monitoring
|
|
738
|
-
await this.setupPageMonitoring(page);
|
|
739
|
-
// Monitor new pages
|
|
740
|
-
this.browserContext.on('page', async (newPage) => {
|
|
741
|
-
// Disable automatic dialog handling for new pages too
|
|
742
|
-
newPage.removeAllListeners('dialog');
|
|
743
|
-
// Add a no-op dialog handler to prevent auto-dismissal
|
|
744
|
-
newPage.on('dialog', async (dialog) => {
|
|
745
|
-
// Don't accept or dismiss - let user handle it manually
|
|
746
|
-
});
|
|
747
|
-
await this.setupPageMonitoring(newPage);
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
async installPlaywrightBrowsers() {
|
|
751
|
-
this.progressBar.update(75, { stage: 'Installing Playwright browser (2-3 min)...' });
|
|
752
|
-
return new Promise((resolve, reject) => {
|
|
753
|
-
const packageManager = detectPackageManager();
|
|
754
|
-
const [command, ...args] = packageManager.split(' ');
|
|
755
|
-
console.log(chalk.gray(`Running: ${command} ${[...args, 'playwright', 'install', 'chromium'].join(' ')}`));
|
|
756
|
-
const installProcess = spawn(command, [...args, 'playwright', 'install', 'chromium'], {
|
|
757
|
-
stdio: ['inherit', 'pipe', 'pipe'],
|
|
758
|
-
shell: true,
|
|
759
|
-
});
|
|
760
|
-
// Add timeout (5 minutes)
|
|
761
|
-
const timeout = setTimeout(() => {
|
|
762
|
-
installProcess.kill('SIGKILL');
|
|
763
|
-
reject(new Error('Playwright installation timed out after 5 minutes'));
|
|
764
|
-
}, 5 * 60 * 1000);
|
|
765
|
-
let hasOutput = false;
|
|
766
|
-
installProcess.stdout?.on('data', (data) => {
|
|
767
|
-
hasOutput = true;
|
|
768
|
-
const message = data.toString().trim();
|
|
769
|
-
if (message) {
|
|
770
|
-
console.log(chalk.gray('[PLAYWRIGHT]'), message);
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
installProcess.stderr?.on('data', (data) => {
|
|
774
|
-
hasOutput = true;
|
|
775
|
-
const message = data.toString().trim();
|
|
776
|
-
if (message) {
|
|
777
|
-
console.log(chalk.gray('[PLAYWRIGHT]'), message);
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
installProcess.on('exit', (code) => {
|
|
781
|
-
clearTimeout(timeout);
|
|
782
|
-
if (code === 0) {
|
|
783
|
-
console.log(chalk.green('✅ Playwright chromium installed successfully!'));
|
|
784
|
-
resolve();
|
|
785
|
-
}
|
|
786
|
-
else {
|
|
787
|
-
reject(new Error(`Playwright installation failed with exit code ${code}`));
|
|
788
|
-
}
|
|
789
|
-
});
|
|
790
|
-
installProcess.on('error', (error) => {
|
|
791
|
-
clearTimeout(timeout);
|
|
792
|
-
reject(new Error(`Failed to start Playwright installation: ${error.message}`));
|
|
793
|
-
});
|
|
794
|
-
// Check if process seems stuck
|
|
795
|
-
setTimeout(() => {
|
|
796
|
-
if (!hasOutput) {
|
|
797
|
-
console.log(chalk.yellow('⚠️ Installation seems stuck. This is normal for the first run - downloading ~100MB...'));
|
|
798
|
-
}
|
|
799
|
-
}, 10000); // Show message after 10 seconds of no output
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
async takeScreenshot(page, event) {
|
|
803
|
-
try {
|
|
804
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
805
|
-
const filename = `${timestamp}-${event}.png`;
|
|
806
|
-
const screenshotPath = join(this.screenshotDir, filename);
|
|
807
|
-
await page.screenshot({
|
|
808
|
-
path: screenshotPath,
|
|
809
|
-
fullPage: false, // Just viewport for speed
|
|
810
|
-
animations: 'disabled' // Disable animations during screenshot
|
|
811
|
-
});
|
|
812
|
-
// Return web-accessible URL (no need to copy since we save directly to MCP public dir)
|
|
813
|
-
return `http://localhost:${this.options.mcpPort}/screenshots/${filename}`;
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
console.error(chalk.red('[SCREENSHOT ERROR]'), error);
|
|
817
|
-
return null;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
async setupPageMonitoring(page) {
|
|
821
|
-
const url = page.url();
|
|
822
|
-
// Only monitor localhost pages
|
|
823
|
-
if (!url.includes(`localhost:${this.options.port}`) && url !== 'about:blank') {
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
this.logger.log('browser', `📄 New page: ${url}`);
|
|
827
|
-
// Console logs
|
|
828
|
-
page.on('console', async (msg) => {
|
|
829
|
-
if (page.url().includes(`localhost:${this.options.port}`)) {
|
|
830
|
-
// Handle our interaction tracking logs specially
|
|
831
|
-
const text = msg.text();
|
|
832
|
-
if (text.startsWith('[DEV3000_INTERACTION]')) {
|
|
833
|
-
const interaction = text.replace('[DEV3000_INTERACTION] ', '');
|
|
834
|
-
this.logger.log('browser', `[INTERACTION] ${interaction}`);
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
// Try to reconstruct the console message properly
|
|
838
|
-
let logMessage;
|
|
839
|
-
try {
|
|
840
|
-
// Get all arguments from the console message
|
|
841
|
-
const args = msg.args();
|
|
842
|
-
if (args.length === 0) {
|
|
843
|
-
logMessage = text;
|
|
844
|
-
}
|
|
845
|
-
else if (args.length === 1) {
|
|
846
|
-
// Single argument - use text() which is already formatted
|
|
847
|
-
logMessage = text;
|
|
848
|
-
}
|
|
849
|
-
else {
|
|
850
|
-
// Multiple arguments - format them properly
|
|
851
|
-
const argValues = await Promise.all(args.map(async (arg) => {
|
|
852
|
-
try {
|
|
853
|
-
const value = await arg.jsonValue();
|
|
854
|
-
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
return '[object]';
|
|
858
|
-
}
|
|
859
|
-
}));
|
|
860
|
-
// Join all arguments with spaces (like normal console output)
|
|
861
|
-
logMessage = argValues.join(' ');
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
catch (error) {
|
|
865
|
-
// Fallback to original text if args processing fails
|
|
866
|
-
logMessage = text;
|
|
867
|
-
}
|
|
868
|
-
const level = msg.type().toUpperCase();
|
|
869
|
-
this.logger.log('browser', `[CONSOLE ${level}] ${logMessage}`);
|
|
870
|
-
}
|
|
871
|
-
});
|
|
872
|
-
// Page errors
|
|
873
|
-
page.on('pageerror', async (error) => {
|
|
874
|
-
if (page.url().includes(`localhost:${this.options.port}`)) {
|
|
875
|
-
const screenshotPath = await this.takeScreenshot(page, 'error');
|
|
876
|
-
this.logger.log('browser', `[PAGE ERROR] ${error.message}`);
|
|
877
|
-
if (screenshotPath) {
|
|
878
|
-
this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
|
|
879
|
-
}
|
|
880
|
-
if (error.stack) {
|
|
881
|
-
this.logger.log('browser', `[PAGE ERROR STACK] ${error.stack}`);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
// Network requests
|
|
886
|
-
page.on('request', (request) => {
|
|
887
|
-
if (page.url().includes(`localhost:${this.options.port}`) && !request.url().includes(`localhost:${this.options.mcpPort}`)) {
|
|
888
|
-
this.logger.log('browser', `[NETWORK REQUEST] ${request.method()} ${request.url()}`);
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
page.on('response', async (response) => {
|
|
892
|
-
if (page.url().includes(`localhost:${this.options.port}`) && !response.url().includes(`localhost:${this.options.mcpPort}`)) {
|
|
893
|
-
const status = response.status();
|
|
894
|
-
const url = response.url();
|
|
895
|
-
if (status >= 400) {
|
|
896
|
-
const screenshotPath = await this.takeScreenshot(page, 'network-error');
|
|
897
|
-
this.logger.log('browser', `[NETWORK ERROR] ${status} ${url}`);
|
|
898
|
-
if (screenshotPath) {
|
|
899
|
-
this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
});
|
|
904
|
-
// Navigation (only screenshot on route changes, not every navigation)
|
|
905
|
-
let lastRoute = '';
|
|
906
|
-
page.on('framenavigated', async (frame) => {
|
|
907
|
-
if (frame === page.mainFrame() && frame.url().includes(`localhost:${this.options.port}`)) {
|
|
908
|
-
const currentRoute = new URL(frame.url()).pathname;
|
|
909
|
-
this.logger.log('browser', `[NAVIGATION] ${frame.url()}`);
|
|
910
|
-
// Only screenshot if route actually changed
|
|
911
|
-
if (currentRoute !== lastRoute) {
|
|
912
|
-
const screenshotPath = await this.takeScreenshot(page, 'route-change');
|
|
913
|
-
if (screenshotPath) {
|
|
914
|
-
this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
|
|
915
|
-
}
|
|
916
|
-
lastRoute = currentRoute;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
});
|
|
920
|
-
// Set up user interaction tracking (clicks, scrolls, etc.)
|
|
921
|
-
await this.setupInteractionTracking(page);
|
|
922
|
-
}
|
|
923
|
-
async setupInteractionTracking(page) {
|
|
924
|
-
if (!page.url().includes(`localhost:${this.options.port}`)) {
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
try {
|
|
928
|
-
// Inject interaction tracking scripts into the page (both at init and after load)
|
|
929
|
-
const trackingScript = () => {
|
|
930
|
-
// Only inject once
|
|
931
|
-
if (window.__dev3000_tracking_injected)
|
|
932
|
-
return;
|
|
933
|
-
window.__dev3000_tracking_injected = true;
|
|
934
|
-
// Track clicks and taps
|
|
935
|
-
document.addEventListener('click', (event) => {
|
|
936
|
-
const target = event.target;
|
|
937
|
-
const targetSelector = target.tagName.toLowerCase() +
|
|
938
|
-
(target.id ? `#${target.id}` : '') +
|
|
939
|
-
(target.className ? `.${target.className.split(' ').join('.')}` : '');
|
|
940
|
-
const interactionData = {
|
|
941
|
-
type: 'CLICK',
|
|
942
|
-
coordinates: { x: event.clientX, y: event.clientY },
|
|
943
|
-
target: targetSelector,
|
|
944
|
-
text: target.textContent?.slice(0, 50) || null,
|
|
945
|
-
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
946
|
-
scroll: { x: window.scrollX, y: window.scrollY }
|
|
947
|
-
};
|
|
948
|
-
console.log(`[DEV3000_INTERACTION] ${JSON.stringify(interactionData)}`);
|
|
949
|
-
}, true);
|
|
950
|
-
// Track touch events (mobile/tablet)
|
|
951
|
-
document.addEventListener('touchstart', (event) => {
|
|
952
|
-
if (event.touches.length > 0) {
|
|
953
|
-
const touch = event.touches[0];
|
|
954
|
-
const target = event.target;
|
|
955
|
-
const targetSelector = target.tagName.toLowerCase() +
|
|
956
|
-
(target.id ? `#${target.id}` : '') +
|
|
957
|
-
(target.className ? `.${target.className.split(' ').join('.')}` : '');
|
|
958
|
-
const interactionData = {
|
|
959
|
-
type: 'TAP',
|
|
960
|
-
coordinates: { x: Math.round(touch.clientX), y: Math.round(touch.clientY) },
|
|
961
|
-
target: targetSelector,
|
|
962
|
-
text: target.textContent?.slice(0, 50) || null,
|
|
963
|
-
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
964
|
-
scroll: { x: window.scrollX, y: window.scrollY }
|
|
965
|
-
};
|
|
966
|
-
console.log(`[DEV3000_INTERACTION] ${JSON.stringify(interactionData)}`);
|
|
967
|
-
}
|
|
968
|
-
}, true);
|
|
969
|
-
// Track scrolling with throttling to avoid spam
|
|
970
|
-
let lastScrollTime = 0;
|
|
971
|
-
let lastScrollY = window.scrollY;
|
|
972
|
-
let lastScrollX = window.scrollX;
|
|
973
|
-
document.addEventListener('scroll', () => {
|
|
974
|
-
const now = Date.now();
|
|
975
|
-
if (now - lastScrollTime > 500) { // Throttle to max once per 500ms
|
|
976
|
-
const deltaY = window.scrollY - lastScrollY;
|
|
977
|
-
const deltaX = window.scrollX - lastScrollX;
|
|
978
|
-
if (Math.abs(deltaY) > 10 || Math.abs(deltaX) > 10) { // Only log significant scrolls
|
|
979
|
-
const direction = deltaY > 0 ? 'DOWN' : deltaY < 0 ? 'UP' : deltaX > 0 ? 'RIGHT' : 'LEFT';
|
|
980
|
-
const distance = Math.round(Math.sqrt(deltaX * deltaX + deltaY * deltaY));
|
|
981
|
-
const interactionData = {
|
|
982
|
-
type: 'SCROLL',
|
|
983
|
-
direction: direction,
|
|
984
|
-
distance: distance,
|
|
985
|
-
from: { x: lastScrollX, y: lastScrollY },
|
|
986
|
-
to: { x: window.scrollX, y: window.scrollY },
|
|
987
|
-
viewport: { width: window.innerWidth, height: window.innerHeight }
|
|
988
|
-
};
|
|
989
|
-
console.log(`[DEV3000_INTERACTION] ${JSON.stringify(interactionData)}`);
|
|
990
|
-
lastScrollTime = now;
|
|
991
|
-
lastScrollY = window.scrollY;
|
|
992
|
-
lastScrollX = window.scrollX;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}, true);
|
|
996
|
-
// Track keyboard events (for form interactions)
|
|
997
|
-
document.addEventListener('keydown', (event) => {
|
|
998
|
-
const target = event.target;
|
|
999
|
-
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') {
|
|
1000
|
-
const targetSelector = target.tagName.toLowerCase() +
|
|
1001
|
-
(target.id ? `#${target.id}` : '') +
|
|
1002
|
-
(target.type ? `[type=${target.type}]` : '') +
|
|
1003
|
-
(target.className ? `.${target.className.split(' ').join('.')}` : '');
|
|
1004
|
-
// Log special keys, but not every character to avoid logging sensitive data
|
|
1005
|
-
if (event.key.length > 1 || event.key === ' ') {
|
|
1006
|
-
const interactionData = {
|
|
1007
|
-
type: 'KEY',
|
|
1008
|
-
key: event.key === ' ' ? 'Space' : event.key,
|
|
1009
|
-
target: targetSelector,
|
|
1010
|
-
modifiers: {
|
|
1011
|
-
ctrl: event.ctrlKey,
|
|
1012
|
-
alt: event.altKey,
|
|
1013
|
-
shift: event.shiftKey,
|
|
1014
|
-
meta: event.metaKey
|
|
1015
|
-
},
|
|
1016
|
-
inputType: target.type || target.tagName.toLowerCase()
|
|
1017
|
-
};
|
|
1018
|
-
console.log(`[DEV3000_INTERACTION] ${JSON.stringify(interactionData)}`);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}, true);
|
|
1022
|
-
// Log that tracking is active
|
|
1023
|
-
console.log('[DEV3000_INTERACTION] Tracking initialized');
|
|
1024
|
-
};
|
|
1025
|
-
// Add to page init - this should be sufficient
|
|
1026
|
-
await page.addInitScript(trackingScript);
|
|
1027
|
-
// Note: Interaction logs will be captured by the existing console handler in setupPageMonitoring
|
|
549
|
+
// Start CDP monitoring
|
|
550
|
+
await this.cdpMonitor.start();
|
|
551
|
+
this.logger.log('browser', '[CDP] Chrome launched with DevTools Protocol monitoring');
|
|
552
|
+
// Navigate to the app
|
|
553
|
+
await this.cdpMonitor.navigateToApp(this.options.port);
|
|
554
|
+
this.logger.log('browser', `[CDP] Navigated to http://localhost:${this.options.port}`);
|
|
1028
555
|
}
|
|
1029
556
|
catch (error) {
|
|
1030
|
-
|
|
557
|
+
// Log error but don't crash - we want the servers to keep running
|
|
558
|
+
this.logger.log('browser', `[CDP ERROR] Failed to start CDP monitoring: ${error}`);
|
|
559
|
+
console.error(chalk.red('⚠️ CDP monitoring failed, but servers are still running'));
|
|
1031
560
|
}
|
|
1032
561
|
}
|
|
1033
562
|
setupCleanupHandlers() {
|
|
@@ -1058,42 +587,15 @@ export class DevEnvironment {
|
|
|
1058
587
|
killPortProcess(this.options.port, 'your app server'),
|
|
1059
588
|
killPortProcess(this.options.mcpPort, 'dev3000 MCP server')
|
|
1060
589
|
]);
|
|
1061
|
-
//
|
|
1062
|
-
this.
|
|
1063
|
-
// Try to save browser state quickly (with timeout) - non-intrusive
|
|
1064
|
-
if (this.browserContext) {
|
|
1065
|
-
try {
|
|
1066
|
-
console.log(chalk.blue('💾 Saving browser state...'));
|
|
1067
|
-
const savePromise = this.saveStateManually();
|
|
1068
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000));
|
|
1069
|
-
await Promise.race([savePromise, timeoutPromise]);
|
|
1070
|
-
console.log(chalk.green('✅ Browser state saved'));
|
|
1071
|
-
}
|
|
1072
|
-
catch (error) {
|
|
1073
|
-
console.log(chalk.gray('⚠️ Could not save browser state (timed out)'));
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
// Close browser quickly (with timeout)
|
|
1077
|
-
if (this.browser) {
|
|
590
|
+
// Shutdown CDP monitor
|
|
591
|
+
if (this.cdpMonitor) {
|
|
1078
592
|
try {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
else {
|
|
1083
|
-
console.log(chalk.blue('🔄 Closing browser...'));
|
|
1084
|
-
}
|
|
1085
|
-
const closePromise = this.browser.close();
|
|
1086
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000));
|
|
1087
|
-
await Promise.race([closePromise, timeoutPromise]);
|
|
1088
|
-
console.log(chalk.green('✅ Browser closed'));
|
|
593
|
+
console.log(chalk.blue('🔄 Closing CDP monitor...'));
|
|
594
|
+
await this.cdpMonitor.shutdown();
|
|
595
|
+
console.log(chalk.green('✅ CDP monitor closed'));
|
|
1089
596
|
}
|
|
1090
597
|
catch (error) {
|
|
1091
|
-
|
|
1092
|
-
console.log(chalk.gray('⚠️ Chrome tab close failed (this is normal - your Chrome stays open)'));
|
|
1093
|
-
}
|
|
1094
|
-
else {
|
|
1095
|
-
console.log(chalk.gray('⚠️ Browser close timed out'));
|
|
1096
|
-
}
|
|
598
|
+
console.log(chalk.gray('⚠️ CDP monitor shutdown failed'));
|
|
1097
599
|
}
|
|
1098
600
|
}
|
|
1099
601
|
console.log(chalk.green('✅ Cleanup complete'));
|