dev3000 0.0.24 → 0.0.26
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/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 +3 -17
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +41 -560
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/replay/route.ts +67 -6
- package/package.json +8 -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();
|
|
@@ -383,131 +376,6 @@ export class DevEnvironment {
|
|
|
383
376
|
}
|
|
384
377
|
});
|
|
385
378
|
}
|
|
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
|
|
510
|
-
}
|
|
511
379
|
async waitForServer() {
|
|
512
380
|
const maxAttempts = 30;
|
|
513
381
|
let attempts = 0;
|
|
@@ -641,393 +509,33 @@ export class DevEnvironment {
|
|
|
641
509
|
}
|
|
642
510
|
// Continue anyway if health check fails
|
|
643
511
|
}
|
|
644
|
-
|
|
645
|
-
// Start
|
|
646
|
-
this.
|
|
647
|
-
console.error(chalk.red('⚠️
|
|
512
|
+
startCDPMonitoringAsync() {
|
|
513
|
+
// Start CDP monitoring in background without blocking completion
|
|
514
|
+
this.startCDPMonitoring().catch(error => {
|
|
515
|
+
console.error(chalk.red('⚠️ CDP monitoring setup failed:'), error);
|
|
648
516
|
});
|
|
649
517
|
}
|
|
650
|
-
async
|
|
518
|
+
async startCDPMonitoring() {
|
|
651
519
|
// Ensure profile directory exists
|
|
652
520
|
if (!existsSync(this.options.profileDir)) {
|
|
653
521
|
mkdirSync(this.options.profileDir, { recursive: true });
|
|
654
522
|
}
|
|
523
|
+
// Initialize CDP monitor with enhanced logging
|
|
524
|
+
this.cdpMonitor = new CDPMonitor(this.options.profileDir, (source, message) => {
|
|
525
|
+
this.logger.log('browser', message);
|
|
526
|
+
}, this.options.debug);
|
|
655
527
|
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';
|
|
528
|
+
// Start CDP monitoring
|
|
529
|
+
await this.cdpMonitor.start();
|
|
530
|
+
this.logger.log('browser', '[CDP] Chrome launched with DevTools Protocol monitoring');
|
|
531
|
+
// Navigate to the app
|
|
532
|
+
await this.cdpMonitor.navigateToApp(this.options.port);
|
|
533
|
+
this.logger.log('browser', `[CDP] Navigated to http://localhost:${this.options.port}`);
|
|
670
534
|
}
|
|
671
535
|
catch (error) {
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
1028
|
-
}
|
|
1029
|
-
catch (error) {
|
|
1030
|
-
console.warn('Could not set up interaction tracking:', error);
|
|
536
|
+
// Log error but don't crash - we want the servers to keep running
|
|
537
|
+
this.logger.log('browser', `[CDP ERROR] Failed to start CDP monitoring: ${error}`);
|
|
538
|
+
console.error(chalk.red('⚠️ CDP monitoring failed, but servers are still running'));
|
|
1031
539
|
}
|
|
1032
540
|
}
|
|
1033
541
|
setupCleanupHandlers() {
|
|
@@ -1058,42 +566,15 @@ export class DevEnvironment {
|
|
|
1058
566
|
killPortProcess(this.options.port, 'your app server'),
|
|
1059
567
|
killPortProcess(this.options.mcpPort, 'dev3000 MCP server')
|
|
1060
568
|
]);
|
|
1061
|
-
//
|
|
1062
|
-
this.
|
|
1063
|
-
// Try to save browser state quickly (with timeout) - non-intrusive
|
|
1064
|
-
if (this.browserContext) {
|
|
569
|
+
// Shutdown CDP monitor
|
|
570
|
+
if (this.cdpMonitor) {
|
|
1065
571
|
try {
|
|
1066
|
-
console.log(chalk.blue('
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
await Promise.race([savePromise, timeoutPromise]);
|
|
1070
|
-
console.log(chalk.green('✅ Browser state saved'));
|
|
572
|
+
console.log(chalk.blue('🔄 Closing CDP monitor...'));
|
|
573
|
+
await this.cdpMonitor.shutdown();
|
|
574
|
+
console.log(chalk.green('✅ CDP monitor closed'));
|
|
1071
575
|
}
|
|
1072
576
|
catch (error) {
|
|
1073
|
-
console.log(chalk.gray('⚠️
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
// Close browser quickly (with timeout)
|
|
1077
|
-
if (this.browser) {
|
|
1078
|
-
try {
|
|
1079
|
-
if (this.browserType === 'system-chrome') {
|
|
1080
|
-
console.log(chalk.blue('🔄 Closing browser tab (keeping Chrome open)...'));
|
|
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'));
|
|
1089
|
-
}
|
|
1090
|
-
catch (error) {
|
|
1091
|
-
if (this.browserType === 'system-chrome') {
|
|
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
|
-
}
|
|
577
|
+
console.log(chalk.gray('⚠️ CDP monitor shutdown failed'));
|
|
1097
578
|
}
|
|
1098
579
|
}
|
|
1099
580
|
console.log(chalk.green('✅ Cleanup complete'));
|