dev3000 0.0.22 → 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.
@@ -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';
@@ -102,10 +93,9 @@ function pruneOldLogs(baseDir, cwdName) {
102
93
  for (const file of filesToDelete) {
103
94
  try {
104
95
  unlinkSync(file.path);
105
- console.log(chalk.gray(`🗑️ Pruned old log: ${file.name}`));
106
96
  }
107
97
  catch (error) {
108
- console.warn(chalk.yellow(`⚠️ Could not delete old log ${file.name}: ${error}`));
98
+ // Silently ignore deletion errors
109
99
  }
110
100
  }
111
101
  }
@@ -117,16 +107,14 @@ function pruneOldLogs(baseDir, cwdName) {
117
107
  export class DevEnvironment {
118
108
  serverProcess = null;
119
109
  mcpServerProcess = null;
120
- browser = null;
121
- browserContext = null;
110
+ cdpMonitor = null;
122
111
  logger;
123
- stateTimer = null;
124
- browserType = null;
125
112
  options;
126
113
  screenshotDir;
127
114
  mcpPublicDir;
128
115
  pidFile;
129
116
  progressBar;
117
+ version;
130
118
  constructor(options) {
131
119
  this.options = options;
132
120
  this.logger = new Logger(options.logFile);
@@ -138,9 +126,33 @@ export class DevEnvironment {
138
126
  this.screenshotDir = join(packageRoot, 'mcp-server', 'public', 'screenshots');
139
127
  this.pidFile = join(tmpdir(), 'dev3000.pid');
140
128
  this.mcpPublicDir = join(packageRoot, 'mcp-server', 'public', 'screenshots');
129
+ // Read version from package.json for startup message
130
+ this.version = '0.0.0';
131
+ try {
132
+ const packageJsonPath = join(packageRoot, 'package.json');
133
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
134
+ this.version = packageJson.version;
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
148
+ }
149
+ }
150
+ catch (error) {
151
+ // Use fallback version
152
+ }
141
153
  // Initialize progress bar
142
154
  this.progressBar = new cliProgress.SingleBar({
143
- format: chalk.blue('Starting dev3000') + ' |' + chalk.cyan('{bar}') + '| {percentage}% | {stage}',
155
+ format: '|' + chalk.cyan('{bar}') + '| {percentage}% | {stage}',
144
156
  barCompleteChar: '█',
145
157
  barIncompleteChar: '░',
146
158
  hideCursor: true,
@@ -154,11 +166,6 @@ export class DevEnvironment {
154
166
  mkdirSync(this.mcpPublicDir, { recursive: true });
155
167
  }
156
168
  }
157
- debugLog(message) {
158
- if (this.options.debug) {
159
- console.log(chalk.gray(`[DEBUG] ${message}`));
160
- }
161
- }
162
169
  async checkPortsAvailable() {
163
170
  const ports = [this.options.port, this.options.mcpPort];
164
171
  for (const port of ports) {
@@ -185,6 +192,8 @@ export class DevEnvironment {
185
192
  }
186
193
  }
187
194
  async start() {
195
+ // Show startup message first
196
+ console.log(chalk.blue(`Starting dev3000 (v${this.version})`));
188
197
  // Start progress bar
189
198
  this.progressBar.start(100, 0, { stage: 'Checking ports...' });
190
199
  // Check if ports are available first
@@ -200,25 +209,19 @@ export class DevEnvironment {
200
209
  // Start MCP server
201
210
  await this.startMcpServer();
202
211
  this.progressBar.update(30, { stage: 'Waiting for your app server...' });
203
- // Animate progress while waiting for servers with realistic increments
204
- const serverWaitPromise = this.waitForServer();
205
- const progressAnimation = this.animateProgress(30, 50, serverWaitPromise, 'app server starting');
206
- await Promise.all([serverWaitPromise, progressAnimation]);
207
- this.progressBar.update(50, { stage: 'Waiting for MCP server...' });
208
- const mcpWaitPromise = this.waitForMcpServer();
209
- const mcpProgressAnimation = this.animateProgress(50, 70, mcpWaitPromise, 'MCP server starting');
210
- await Promise.all([mcpWaitPromise, mcpProgressAnimation]);
211
- this.progressBar.update(70, { stage: 'Starting browser...' });
212
- // Start browser monitoring
213
- const browserPromise = this.startBrowserMonitoring();
214
- const browserProgressAnimation = this.animateProgress(70, 100, browserPromise, 'browser starting');
215
- await Promise.all([browserPromise, browserProgressAnimation]);
212
+ // Wait for servers to be ready (no artificial delays)
213
+ await this.waitForServer();
214
+ this.progressBar.update(60, { stage: 'Waiting for MCP server...' });
215
+ await this.waitForMcpServer();
216
+ this.progressBar.update(80, { stage: 'Starting browser...' });
217
+ // Start CDP monitoring but don't wait for full setup
218
+ this.startCDPMonitoringAsync();
216
219
  this.progressBar.update(100, { stage: 'Complete!' });
217
- // Stop progress bar and show results
220
+ // Stop progress bar and show results immediately
218
221
  this.progressBar.stop();
219
222
  console.log(chalk.green('\n✅ Development environment ready!'));
220
- console.log(chalk.blue(`📊 Logs: ${this.options.logFile}`));
221
- console.log(chalk.gray(`🔧 MCP Server Logs: ${join(dirname(this.options.logFile), 'dev3000-mcp.log')}`));
223
+ console.log(chalk.blue(`Logs: ${this.options.logFile}`));
224
+ console.log(chalk.blue(`Logs symlink: /tmp/dev3000.log`));
222
225
  console.log(chalk.yellow('☝️ Give this to an AI to auto debug and fix your app\n'));
223
226
  console.log(chalk.blue(`🌐 Your App: http://localhost:${this.options.port}`));
224
227
  console.log(chalk.blue(`🤖 MCP Server: http://localhost:${this.options.mcpPort}/api/mcp/http`));
@@ -276,6 +279,11 @@ export class DevEnvironment {
276
279
  const tmpDirPath = join(tmpdir(), 'dev3000-mcp-deps');
277
280
  nodeModulesPath = join(tmpDirPath, 'node_modules');
278
281
  actualWorkingDir = tmpDirPath;
282
+ // Update screenshot directory to use the temp directory for global installs
283
+ this.screenshotDir = join(actualWorkingDir, 'public', 'screenshots');
284
+ if (!existsSync(this.screenshotDir)) {
285
+ mkdirSync(this.screenshotDir, { recursive: true });
286
+ }
279
287
  }
280
288
  if (!existsSync(nodeModulesPath)) {
281
289
  // Hide progress bar during installation
@@ -286,22 +294,7 @@ export class DevEnvironment {
286
294
  // Resume progress bar
287
295
  this.progressBar.start(100, 20, { stage: 'Starting MCP server...' });
288
296
  }
289
- // Read version from package.json
290
- const versionCurrentFile = fileURLToPath(import.meta.url);
291
- const versionPackageRoot = dirname(dirname(versionCurrentFile));
292
- const packageJsonPath = join(versionPackageRoot, 'package.json');
293
- let version = '0.0.0';
294
- try {
295
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
296
- version = packageJson.version;
297
- // Add -dev suffix for local development when running from symlinked source
298
- if (process.cwd().includes('vercel-labs/dev3000')) {
299
- version += '-dev';
300
- }
301
- }
302
- catch (error) {
303
- console.log(chalk.yellow('⚠️ Could not read version from package.json'));
304
- }
297
+ // Use version already read in constructor
305
298
  // For global installs, ensure all necessary files are copied to temp directory
306
299
  if (isGlobalInstall && actualWorkingDir !== mcpServerPath) {
307
300
  const requiredFiles = ['app', 'public', 'next.config.ts', 'next-env.d.ts', 'tsconfig.json'];
@@ -352,7 +345,7 @@ export class DevEnvironment {
352
345
  ...process.env,
353
346
  PORT: this.options.mcpPort,
354
347
  LOG_FILE_PATH: this.options.logFile, // Pass log file path to MCP server
355
- DEV3000_VERSION: version, // Pass version to MCP server
348
+ DEV3000_VERSION: this.version, // Pass version to MCP server
356
349
  },
357
350
  });
358
351
  // Log MCP server output to separate file for debugging
@@ -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;
@@ -529,31 +397,6 @@ export class DevEnvironment {
529
397
  }
530
398
  // Continue anyway if health check fails
531
399
  }
532
- async animateProgress(startPercent, endPercent, waitPromise, stage) {
533
- const duration = 15000; // 15 seconds max animation
534
- const interval = 200; // Update every 200ms
535
- const totalSteps = duration / interval;
536
- let currentPercent = startPercent;
537
- let step = 0;
538
- const animationInterval = setInterval(() => {
539
- if (step < totalSteps) {
540
- // Use easing function for more realistic progress
541
- const progress = step / totalSteps;
542
- const easedProgress = 1 - Math.pow(1 - progress, 3); // Ease-out cubic
543
- currentPercent = startPercent + (endPercent - startPercent) * easedProgress;
544
- this.progressBar.update(Math.min(currentPercent, endPercent - 1), {
545
- stage: `${stage}... ${Math.floor(currentPercent)}%`
546
- });
547
- step++;
548
- }
549
- }, interval);
550
- try {
551
- await waitPromise;
552
- }
553
- finally {
554
- clearInterval(animationInterval);
555
- }
556
- }
557
400
  async installMcpServerDeps(mcpServerPath) {
558
401
  return new Promise((resolve, reject) => {
559
402
  // For global installs, we need to install to a writable location
@@ -666,346 +509,33 @@ export class DevEnvironment {
666
509
  }
667
510
  // Continue anyway if health check fails
668
511
  }
669
- async startBrowserMonitoring() {
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);
516
+ });
517
+ }
518
+ async startCDPMonitoring() {
670
519
  // Ensure profile directory exists
671
520
  if (!existsSync(this.options.profileDir)) {
672
521
  mkdirSync(this.options.profileDir, { recursive: true });
673
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);
674
527
  try {
675
- // Try to use system Chrome first
676
- this.browser = await chromium.launch({
677
- headless: false,
678
- channel: 'chrome', // Use system Chrome
679
- // Remove automation flags to allow normal dialog behavior
680
- args: [
681
- '--disable-web-security', // Keep this for dev server access
682
- '--hide-crash-restore-bubble', // Don't ask to restore pages
683
- '--disable-infobars', // Remove info bars
684
- '--disable-blink-features=AutomationControlled', // Hide automation detection
685
- '--disable-features=VizDisplayCompositor', // Reduce automation fingerprinting
686
- ],
687
- });
688
- this.browserType = 'system-chrome';
689
- }
690
- catch (error) {
691
- // Fallback to Playwright's bundled chromium
692
- try {
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
- '--disable-blink-features=AutomationControlled', // Hide automation detection
701
- '--disable-features=VizDisplayCompositor', // Reduce automation fingerprinting
702
- ],
703
- });
704
- this.browserType = 'playwright-chromium';
705
- }
706
- catch (playwrightError) {
707
- if (playwrightError.message?.includes('Executable doesn\'t exist')) {
708
- detectPackageManager();
709
- console.log(chalk.yellow('📦 Installing Playwright chromium browser...'));
710
- await this.installPlaywrightBrowsers();
711
- // Retry with bundled chromium
712
- this.browser = await chromium.launch({
713
- headless: false,
714
- // Remove automation flags to allow normal dialog behavior
715
- args: [
716
- '--disable-web-security', // Keep this for dev server access
717
- '--hide-crash-restore-bubble', // Don't ask to restore pages
718
- '--disable-infobars', // Remove info bars
719
- ],
720
- });
721
- this.browserType = 'playwright-chromium';
722
- }
723
- else {
724
- throw playwrightError;
725
- }
726
- }
727
- }
728
- // Create context with viewport: null to enable window resizing
729
- this.browserContext = await this.browser.newContext({
730
- viewport: null, // This makes the page size depend on the window size
731
- });
732
- // Restore state manually (non-intrusive)
733
- await this.loadStateManually();
734
- // Set up focus-aware periodic storage state saving
735
- this.startStateSaving();
736
- // Navigate to the app using the existing blank page
737
- const pages = this.browserContext.pages();
738
- const page = pages.length > 0 ? pages[0] : await this.browserContext.newPage();
739
- // Disable automatic dialog handling - let dialogs behave naturally
740
- page.removeAllListeners('dialog');
741
- // Add a no-op dialog handler to prevent auto-dismissal
742
- page.on('dialog', async (dialog) => {
743
- // Don't accept or dismiss - let user handle it manually
744
- // This prevents Playwright from auto-handling the dialog
745
- });
746
- await page.goto(`http://localhost:${this.options.port}`);
747
- // Restore localStorage and sessionStorage after navigation
748
- await this.restoreStorageData(page);
749
- // Set up focus detection after navigation to prevent context execution errors
750
- await this.setupFocusHandlers(page);
751
- // Take initial screenshot
752
- const initialScreenshot = await this.takeScreenshot(page, 'initial-load');
753
- if (initialScreenshot) {
754
- this.logger.log('browser', `[SCREENSHOT] ${initialScreenshot}`);
755
- }
756
- // Set up monitoring
757
- await this.setupPageMonitoring(page);
758
- // Monitor new pages
759
- this.browserContext.on('page', async (newPage) => {
760
- // Disable automatic dialog handling for new pages too
761
- newPage.removeAllListeners('dialog');
762
- // Add a no-op dialog handler to prevent auto-dismissal
763
- newPage.on('dialog', async (dialog) => {
764
- // Don't accept or dismiss - let user handle it manually
765
- });
766
- await this.setupPageMonitoring(newPage);
767
- });
768
- }
769
- async installPlaywrightBrowsers() {
770
- this.progressBar.update(75, { stage: 'Installing Playwright browser (2-3 min)...' });
771
- return new Promise((resolve, reject) => {
772
- const packageManager = detectPackageManager();
773
- const [command, ...args] = packageManager.split(' ');
774
- console.log(chalk.gray(`Running: ${command} ${[...args, 'playwright', 'install', 'chromium'].join(' ')}`));
775
- const installProcess = spawn(command, [...args, 'playwright', 'install', 'chromium'], {
776
- stdio: ['inherit', 'pipe', 'pipe'],
777
- shell: true,
778
- });
779
- // Add timeout (5 minutes)
780
- const timeout = setTimeout(() => {
781
- installProcess.kill('SIGKILL');
782
- reject(new Error('Playwright installation timed out after 5 minutes'));
783
- }, 5 * 60 * 1000);
784
- let hasOutput = false;
785
- installProcess.stdout?.on('data', (data) => {
786
- hasOutput = true;
787
- const message = data.toString().trim();
788
- if (message) {
789
- console.log(chalk.gray('[PLAYWRIGHT]'), message);
790
- }
791
- });
792
- installProcess.stderr?.on('data', (data) => {
793
- hasOutput = true;
794
- const message = data.toString().trim();
795
- if (message) {
796
- console.log(chalk.gray('[PLAYWRIGHT]'), message);
797
- }
798
- });
799
- installProcess.on('exit', (code) => {
800
- clearTimeout(timeout);
801
- if (code === 0) {
802
- console.log(chalk.green('✅ Playwright chromium installed successfully!'));
803
- resolve();
804
- }
805
- else {
806
- reject(new Error(`Playwright installation failed with exit code ${code}`));
807
- }
808
- });
809
- installProcess.on('error', (error) => {
810
- clearTimeout(timeout);
811
- reject(new Error(`Failed to start Playwright installation: ${error.message}`));
812
- });
813
- // Check if process seems stuck
814
- setTimeout(() => {
815
- if (!hasOutput) {
816
- console.log(chalk.yellow('⚠️ Installation seems stuck. This is normal for the first run - downloading ~100MB...'));
817
- }
818
- }, 10000); // Show message after 10 seconds of no output
819
- });
820
- }
821
- async takeScreenshot(page, event) {
822
- try {
823
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
824
- const filename = `${timestamp}-${event}.png`;
825
- const screenshotPath = join(this.screenshotDir, filename);
826
- await page.screenshot({
827
- path: screenshotPath,
828
- fullPage: false, // Just viewport for speed
829
- animations: 'disabled' // Disable animations during screenshot
830
- });
831
- // Return web-accessible URL (no need to copy since we save directly to MCP public dir)
832
- return `http://localhost:${this.options.mcpPort}/screenshots/${filename}`;
833
- }
834
- catch (error) {
835
- console.error(chalk.red('[SCREENSHOT ERROR]'), error);
836
- return null;
837
- }
838
- }
839
- async setupPageMonitoring(page) {
840
- const url = page.url();
841
- // Only monitor localhost pages
842
- if (!url.includes(`localhost:${this.options.port}`) && url !== 'about:blank') {
843
- return;
844
- }
845
- this.logger.log('browser', `📄 New page: ${url}`);
846
- // Console logs
847
- page.on('console', async (msg) => {
848
- if (page.url().includes(`localhost:${this.options.port}`)) {
849
- // Handle our interaction tracking logs specially
850
- const text = msg.text();
851
- if (text.startsWith('[DEV3000_INTERACTION]')) {
852
- const interaction = text.replace('[DEV3000_INTERACTION] ', '');
853
- this.logger.log('browser', `[INTERACTION] ${interaction}`);
854
- return;
855
- }
856
- // Try to reconstruct the console message properly
857
- let logMessage;
858
- try {
859
- // Get all arguments from the console message
860
- const args = msg.args();
861
- if (args.length === 0) {
862
- logMessage = text;
863
- }
864
- else if (args.length === 1) {
865
- // Single argument - use text() which is already formatted
866
- logMessage = text;
867
- }
868
- else {
869
- // Multiple arguments - format them properly
870
- const argValues = await Promise.all(args.map(async (arg) => {
871
- try {
872
- const value = await arg.jsonValue();
873
- return typeof value === 'object' ? JSON.stringify(value) : String(value);
874
- }
875
- catch {
876
- return '[object]';
877
- }
878
- }));
879
- // Join all arguments with spaces (like normal console output)
880
- logMessage = argValues.join(' ');
881
- }
882
- }
883
- catch (error) {
884
- // Fallback to original text if args processing fails
885
- logMessage = text;
886
- }
887
- const level = msg.type().toUpperCase();
888
- this.logger.log('browser', `[CONSOLE ${level}] ${logMessage}`);
889
- }
890
- });
891
- // Page errors
892
- page.on('pageerror', async (error) => {
893
- if (page.url().includes(`localhost:${this.options.port}`)) {
894
- const screenshotPath = await this.takeScreenshot(page, 'error');
895
- this.logger.log('browser', `[PAGE ERROR] ${error.message}`);
896
- if (screenshotPath) {
897
- this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
898
- }
899
- if (error.stack) {
900
- this.logger.log('browser', `[PAGE ERROR STACK] ${error.stack}`);
901
- }
902
- }
903
- });
904
- // Network requests
905
- page.on('request', (request) => {
906
- if (page.url().includes(`localhost:${this.options.port}`) && !request.url().includes(`localhost:${this.options.mcpPort}`)) {
907
- this.logger.log('browser', `[NETWORK REQUEST] ${request.method()} ${request.url()}`);
908
- }
909
- });
910
- page.on('response', async (response) => {
911
- if (page.url().includes(`localhost:${this.options.port}`) && !response.url().includes(`localhost:${this.options.mcpPort}`)) {
912
- const status = response.status();
913
- const url = response.url();
914
- if (status >= 400) {
915
- const screenshotPath = await this.takeScreenshot(page, 'network-error');
916
- this.logger.log('browser', `[NETWORK ERROR] ${status} ${url}`);
917
- if (screenshotPath) {
918
- this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
919
- }
920
- }
921
- }
922
- });
923
- // Navigation (only screenshot on route changes, not every navigation)
924
- let lastRoute = '';
925
- page.on('framenavigated', async (frame) => {
926
- if (frame === page.mainFrame() && frame.url().includes(`localhost:${this.options.port}`)) {
927
- const currentRoute = new URL(frame.url()).pathname;
928
- this.logger.log('browser', `[NAVIGATION] ${frame.url()}`);
929
- // Only screenshot if route actually changed
930
- if (currentRoute !== lastRoute) {
931
- const screenshotPath = await this.takeScreenshot(page, 'route-change');
932
- if (screenshotPath) {
933
- this.logger.log('browser', `[SCREENSHOT] ${screenshotPath}`);
934
- }
935
- lastRoute = currentRoute;
936
- }
937
- }
938
- });
939
- // Set up user interaction tracking (clicks, scrolls, etc.)
940
- await this.setupInteractionTracking(page);
941
- }
942
- async setupInteractionTracking(page) {
943
- if (!page.url().includes(`localhost:${this.options.port}`)) {
944
- return;
945
- }
946
- try {
947
- // Inject interaction tracking scripts into the page
948
- await page.addInitScript(() => {
949
- // Track clicks and taps
950
- document.addEventListener('click', (event) => {
951
- const target = event.target;
952
- const targetInfo = target.tagName.toLowerCase() +
953
- (target.id ? `#${target.id}` : '') +
954
- (target.className ? `.${target.className.split(' ').join('.')}` : '');
955
- console.log(`[DEV3000_INTERACTION] CLICK at (${event.clientX}, ${event.clientY}) on ${targetInfo}`);
956
- }, true);
957
- // Track touch events (mobile/tablet)
958
- document.addEventListener('touchstart', (event) => {
959
- if (event.touches.length > 0) {
960
- const touch = event.touches[0];
961
- const target = event.target;
962
- const targetInfo = target.tagName.toLowerCase() +
963
- (target.id ? `#${target.id}` : '') +
964
- (target.className ? `.${target.className.split(' ').join('.')}` : '');
965
- console.log(`[DEV3000_INTERACTION] TAP at (${Math.round(touch.clientX)}, ${Math.round(touch.clientY)}) on ${targetInfo}`);
966
- }
967
- }, true);
968
- // Track scrolling with throttling to avoid spam
969
- let lastScrollTime = 0;
970
- let lastScrollY = window.scrollY;
971
- let lastScrollX = window.scrollX;
972
- document.addEventListener('scroll', () => {
973
- const now = Date.now();
974
- if (now - lastScrollTime > 500) { // Throttle to max once per 500ms
975
- const deltaY = window.scrollY - lastScrollY;
976
- const deltaX = window.scrollX - lastScrollX;
977
- if (Math.abs(deltaY) > 10 || Math.abs(deltaX) > 10) { // Only log significant scrolls
978
- const direction = deltaY > 0 ? 'DOWN' : deltaY < 0 ? 'UP' : deltaX > 0 ? 'RIGHT' : 'LEFT';
979
- const distance = Math.round(Math.sqrt(deltaX * deltaX + deltaY * deltaY));
980
- console.log(`[DEV3000_INTERACTION] SCROLL ${direction} ${distance}px to (${window.scrollX}, ${window.scrollY})`);
981
- lastScrollTime = now;
982
- lastScrollY = window.scrollY;
983
- lastScrollX = window.scrollX;
984
- }
985
- }
986
- }, true);
987
- // Track keyboard events (for form interactions)
988
- document.addEventListener('keydown', (event) => {
989
- const target = event.target;
990
- if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true') {
991
- const targetInfo = target.tagName.toLowerCase() +
992
- (target.id ? `#${target.id}` : '') +
993
- (target.type ? `[type=${target.type}]` : '') +
994
- (target.className ? `.${target.className.split(' ').join('.')}` : '');
995
- // Log special keys, but not every character to avoid logging sensitive data
996
- if (event.key.length > 1) { // Special keys like 'Enter', 'Tab', 'Backspace', etc.
997
- console.log(`[DEV3000_INTERACTION] KEY ${event.key} in ${targetInfo}`);
998
- }
999
- else if (event.key === ' ') {
1000
- console.log(`[DEV3000_INTERACTION] KEY Space in ${targetInfo}`);
1001
- }
1002
- }
1003
- }, true);
1004
- });
1005
- // Note: Interaction logs will be captured by the existing console handler in setupPageMonitoring
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}`);
1006
534
  }
1007
535
  catch (error) {
1008
- 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'));
1009
539
  }
1010
540
  }
1011
541
  setupCleanupHandlers() {
@@ -1036,42 +566,15 @@ export class DevEnvironment {
1036
566
  killPortProcess(this.options.port, 'your app server'),
1037
567
  killPortProcess(this.options.mcpPort, 'dev3000 MCP server')
1038
568
  ]);
1039
- // Clear the state saving timer
1040
- this.stopStateSaving();
1041
- // Try to save browser state quickly (with timeout) - non-intrusive
1042
- if (this.browserContext) {
1043
- try {
1044
- console.log(chalk.blue('💾 Saving browser state...'));
1045
- const savePromise = this.saveStateManually();
1046
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000));
1047
- await Promise.race([savePromise, timeoutPromise]);
1048
- console.log(chalk.green('✅ Browser state saved'));
1049
- }
1050
- catch (error) {
1051
- console.log(chalk.gray('⚠️ Could not save browser state (timed out)'));
1052
- }
1053
- }
1054
- // Close browser quickly (with timeout)
1055
- if (this.browser) {
569
+ // Shutdown CDP monitor
570
+ if (this.cdpMonitor) {
1056
571
  try {
1057
- if (this.browserType === 'system-chrome') {
1058
- console.log(chalk.blue('🔄 Closing browser tab (keeping Chrome open)...'));
1059
- }
1060
- else {
1061
- console.log(chalk.blue('🔄 Closing browser...'));
1062
- }
1063
- const closePromise = this.browser.close();
1064
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000));
1065
- await Promise.race([closePromise, timeoutPromise]);
1066
- console.log(chalk.green('✅ Browser closed'));
572
+ console.log(chalk.blue('🔄 Closing CDP monitor...'));
573
+ await this.cdpMonitor.shutdown();
574
+ console.log(chalk.green('✅ CDP monitor closed'));
1067
575
  }
1068
576
  catch (error) {
1069
- if (this.browserType === 'system-chrome') {
1070
- console.log(chalk.gray('⚠️ Chrome tab close failed (this is normal - your Chrome stays open)'));
1071
- }
1072
- else {
1073
- console.log(chalk.gray('⚠️ Browser close timed out'));
1074
- }
577
+ console.log(chalk.gray('⚠️ CDP monitor shutdown failed'));
1075
578
  }
1076
579
  }
1077
580
  console.log(chalk.green('✅ Cleanup complete'));