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.
- 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 +16 -4
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +4 -17
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +74 -571
- package/dist/dev-environment.js.map +1 -1
- package/mcp-server/app/api/replay/route.ts +412 -0
- package/mcp-server/app/logs/LogsClient.tsx +308 -39
- package/mcp-server/app/replay/ReplayClient.tsx +274 -0
- package/mcp-server/app/replay/page.tsx +5 -0
- 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';
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
await
|
|
207
|
-
this.progressBar.update(
|
|
208
|
-
|
|
209
|
-
|
|
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(
|
|
221
|
-
console.log(chalk.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1040
|
-
this.
|
|
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
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
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'));
|