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.
@@ -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
- browser = null;
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
- // Add -dev suffix for local development installs
148
- if (packageRoot.includes('vercel-labs/dev3000')) {
149
- this.version += '-dev';
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 browser monitoring but don't wait for full setup
225
- this.startBrowserMonitoringAsync();
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/http`));
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
- if (!existsSync(nodeModulesPath)) {
296
- // Hide progress bar during installation
297
- this.progressBar.stop();
298
- console.log(chalk.blue('\n📦 Installing MCP server dependencies (first time only)...'));
299
- await this.installMcpServerDeps(mcpServerPath);
300
- console.log(''); // Add spacing
301
- // Resume progress bar
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
- if (response.ok || response.status === 404) {
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
- // MCP server not ready yet, continue waiting
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
- // Continue anyway if health check fails
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
- startBrowserMonitoringAsync() {
645
- // Start browser monitoring in background without blocking completion
646
- this.startBrowserMonitoring().catch(error => {
647
- console.error(chalk.red('⚠️ Browser monitoring setup failed:'), error);
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 startBrowserMonitoring() {
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
- // Try to use system Chrome first
657
- this.browser = await chromium.launch({
658
- headless: false,
659
- channel: 'chrome', // Use system Chrome
660
- // Remove automation flags to allow normal dialog behavior
661
- args: [
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
- console.warn('Could not set up interaction tracking:', error);
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
- // Clear the state saving timer
1062
- this.stopStateSaving();
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
- 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'));
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
- 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
- }
598
+ console.log(chalk.gray('⚠️ CDP monitor shutdown failed'));
1097
599
  }
1098
600
  }
1099
601
  console.log(chalk.green('✅ Cleanup complete'));