@chinchillaenterprises/mcp-dev-logger 2.1.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -4
- package/dist/index.js +812 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync,
|
|
|
8
8
|
import { join, resolve } from "path";
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
10
|
import { promisify } from "util";
|
|
11
|
+
import { chromium } from "playwright";
|
|
11
12
|
const execAsync = promisify(exec);
|
|
12
13
|
// Schema definitions
|
|
13
14
|
const StartLogStreamingArgsSchema = z.object({
|
|
@@ -47,6 +48,16 @@ const CheckRunningProcessesArgsSchema = z.object({
|
|
|
47
48
|
const DiscoverLogsArgsSchema = z.object({
|
|
48
49
|
sessionDate: z.string().optional().describe("Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)")
|
|
49
50
|
});
|
|
51
|
+
const StartFrontendWithBrowserArgsSchema = z.object({
|
|
52
|
+
command: z.string().optional().describe("Dev command to run (default: npm run dev)"),
|
|
53
|
+
port: z.number().optional().describe("Port to wait for (default: auto-detect from output)"),
|
|
54
|
+
waitTimeout: z.number().optional().describe("Max time to wait for server in ms (default: 30000)"),
|
|
55
|
+
browserDelay: z.number().optional().describe("Delay after server ready in ms (default: 1000)"),
|
|
56
|
+
teachingMode: z.boolean().optional().describe("Enable teaching features (default: true)"),
|
|
57
|
+
processId: z.string().optional().describe("Custom process ID (default: frontend-with-browser)"),
|
|
58
|
+
env: z.record(z.string()).optional().describe("Environment variables"),
|
|
59
|
+
cwd: z.string().optional().describe("Working directory")
|
|
60
|
+
});
|
|
50
61
|
class DevLoggerServer {
|
|
51
62
|
server;
|
|
52
63
|
activeServers;
|
|
@@ -438,9 +449,13 @@ class DevLoggerServer {
|
|
|
438
449
|
console.error('Log cleanup error:', error);
|
|
439
450
|
}
|
|
440
451
|
}
|
|
441
|
-
killAllProcesses() {
|
|
452
|
+
async killAllProcesses() {
|
|
442
453
|
for (const [id, info] of this.activeServers) {
|
|
443
454
|
try {
|
|
455
|
+
// Close browser if running
|
|
456
|
+
if (info.browser) {
|
|
457
|
+
await info.browser.close();
|
|
458
|
+
}
|
|
444
459
|
if (info.process) {
|
|
445
460
|
// Remove all event listeners to prevent memory leaks
|
|
446
461
|
info.process.stdout?.removeAllListeners('data');
|
|
@@ -633,6 +648,93 @@ class DevLoggerServer {
|
|
|
633
648
|
}
|
|
634
649
|
}
|
|
635
650
|
}
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "dev_launch_test_browser",
|
|
654
|
+
description: "Launch a TEST BROWSER for students to interact with. All console logs from this browser are captured automatically. Students should use THIS browser (not their regular browser) to test their app.",
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: "object",
|
|
657
|
+
properties: {
|
|
658
|
+
processId: {
|
|
659
|
+
type: "string",
|
|
660
|
+
description: "Process ID to attach browser console to"
|
|
661
|
+
},
|
|
662
|
+
browserUrl: {
|
|
663
|
+
type: "string",
|
|
664
|
+
description: "URL to open in test browser (default: http://localhost:3000)"
|
|
665
|
+
},
|
|
666
|
+
teachingMode: {
|
|
667
|
+
type: "boolean",
|
|
668
|
+
description: "Enable teaching mode with DevTools open and slower actions (default: true)"
|
|
669
|
+
},
|
|
670
|
+
viewport: {
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {
|
|
673
|
+
width: { type: "number", default: 1280 },
|
|
674
|
+
height: { type: "number", default: 800 }
|
|
675
|
+
},
|
|
676
|
+
description: "Browser window size"
|
|
677
|
+
},
|
|
678
|
+
highlightErrors: {
|
|
679
|
+
type: "boolean",
|
|
680
|
+
description: "Add visual indicator when console errors occur (default: true)"
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
name: "dev_stop_browser_console",
|
|
687
|
+
description: "Stop browser console capture for a specific process",
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: "object",
|
|
690
|
+
properties: {
|
|
691
|
+
processId: {
|
|
692
|
+
type: "string",
|
|
693
|
+
description: "Process ID to stop browser console capture for"
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: "dev_start_frontend_with_browser",
|
|
700
|
+
description: "🚀 ONE-CLICK STUDENT WORKFLOW: Start frontend server AND automatically launch test browser when ready. Perfect for teaching environments - reduces setup from 3 commands to 1. Browser launches with DevTools open, error highlighting, and all console logs captured.",
|
|
701
|
+
inputSchema: {
|
|
702
|
+
type: "object",
|
|
703
|
+
properties: {
|
|
704
|
+
command: {
|
|
705
|
+
type: "string",
|
|
706
|
+
description: "Dev command to run (default: npm run dev)"
|
|
707
|
+
},
|
|
708
|
+
port: {
|
|
709
|
+
type: "number",
|
|
710
|
+
description: "Port to wait for (default: auto-detect from output)"
|
|
711
|
+
},
|
|
712
|
+
waitTimeout: {
|
|
713
|
+
type: "number",
|
|
714
|
+
description: "Max time to wait for server in ms (default: 30000)"
|
|
715
|
+
},
|
|
716
|
+
browserDelay: {
|
|
717
|
+
type: "number",
|
|
718
|
+
description: "Delay after server ready in ms (default: 1000)"
|
|
719
|
+
},
|
|
720
|
+
teachingMode: {
|
|
721
|
+
type: "boolean",
|
|
722
|
+
description: "Enable teaching features (default: true)"
|
|
723
|
+
},
|
|
724
|
+
processId: {
|
|
725
|
+
type: "string",
|
|
726
|
+
description: "Custom process ID (default: frontend-with-browser)"
|
|
727
|
+
},
|
|
728
|
+
env: {
|
|
729
|
+
type: "object",
|
|
730
|
+
description: "Environment variables"
|
|
731
|
+
},
|
|
732
|
+
cwd: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "Working directory"
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
636
738
|
}
|
|
637
739
|
];
|
|
638
740
|
return { tools };
|
|
@@ -843,6 +945,10 @@ class DevLoggerServer {
|
|
|
843
945
|
};
|
|
844
946
|
}
|
|
845
947
|
try {
|
|
948
|
+
// Close browser if running
|
|
949
|
+
if (serverInfo.browser) {
|
|
950
|
+
await serverInfo.browser.close();
|
|
951
|
+
}
|
|
846
952
|
// Remove all event listeners to prevent memory leaks
|
|
847
953
|
if (serverInfo.process) {
|
|
848
954
|
serverInfo.process.stdout?.removeAllListeners('data');
|
|
@@ -890,6 +996,10 @@ class DevLoggerServer {
|
|
|
890
996
|
const stoppedProcesses = [];
|
|
891
997
|
for (const [processId, serverInfo] of this.activeServers) {
|
|
892
998
|
try {
|
|
999
|
+
// Close browser if running
|
|
1000
|
+
if (serverInfo.browser) {
|
|
1001
|
+
await serverInfo.browser.close();
|
|
1002
|
+
}
|
|
893
1003
|
// Remove all event listeners to prevent memory leaks
|
|
894
1004
|
if (serverInfo.process) {
|
|
895
1005
|
serverInfo.process.stdout?.removeAllListeners('data');
|
|
@@ -1233,6 +1343,707 @@ class DevLoggerServer {
|
|
|
1233
1343
|
throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
|
|
1234
1344
|
}
|
|
1235
1345
|
}
|
|
1346
|
+
case "dev_launch_test_browser": {
|
|
1347
|
+
const validatedArgs = z.object({
|
|
1348
|
+
processId: z.string().optional(),
|
|
1349
|
+
browserUrl: z.string().optional(),
|
|
1350
|
+
teachingMode: z.boolean().optional(),
|
|
1351
|
+
viewport: z.object({
|
|
1352
|
+
width: z.number().default(1280),
|
|
1353
|
+
height: z.number().default(800)
|
|
1354
|
+
}).optional(),
|
|
1355
|
+
highlightErrors: z.boolean().optional()
|
|
1356
|
+
}).parse(args);
|
|
1357
|
+
try {
|
|
1358
|
+
// Find the process to attach to
|
|
1359
|
+
const processId = this.findProcessOrDefault(validatedArgs.processId);
|
|
1360
|
+
if (!processId) {
|
|
1361
|
+
return {
|
|
1362
|
+
content: [
|
|
1363
|
+
{
|
|
1364
|
+
type: "text",
|
|
1365
|
+
text: JSON.stringify({
|
|
1366
|
+
status: "error",
|
|
1367
|
+
message: "No dev server process found. Start a dev server first with dev_start_log_streaming"
|
|
1368
|
+
}, null, 2)
|
|
1369
|
+
}
|
|
1370
|
+
]
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const serverInfo = this.activeServers.get(processId);
|
|
1374
|
+
// Check if browser already running for this process
|
|
1375
|
+
if (serverInfo.browser) {
|
|
1376
|
+
return {
|
|
1377
|
+
content: [
|
|
1378
|
+
{
|
|
1379
|
+
type: "text",
|
|
1380
|
+
text: JSON.stringify({
|
|
1381
|
+
status: "already_running",
|
|
1382
|
+
message: `Browser console capture already active for process '${processId}'`
|
|
1383
|
+
}, null, 2)
|
|
1384
|
+
}
|
|
1385
|
+
]
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
// Launch browser with student-friendly defaults
|
|
1389
|
+
const teachingMode = validatedArgs.teachingMode !== false;
|
|
1390
|
+
const viewport = validatedArgs.viewport || { width: 1280, height: 800 };
|
|
1391
|
+
const browser = await chromium.launch({
|
|
1392
|
+
headless: false, // Always visible for students
|
|
1393
|
+
slowMo: teachingMode ? 50 : 0, // Slow down actions in teaching mode
|
|
1394
|
+
devtools: teachingMode, // Open DevTools in teaching mode
|
|
1395
|
+
args: [
|
|
1396
|
+
`--window-size=${viewport.width},${viewport.height}`,
|
|
1397
|
+
'--window-position=100,100',
|
|
1398
|
+
'--disable-features=RendererCodeIntegrity' // Prevent some security warnings
|
|
1399
|
+
]
|
|
1400
|
+
});
|
|
1401
|
+
const context = await browser.newContext({
|
|
1402
|
+
viewport: viewport,
|
|
1403
|
+
ignoreHTTPSErrors: true // Common in dev environments
|
|
1404
|
+
});
|
|
1405
|
+
const page = await context.newPage();
|
|
1406
|
+
// Add page title to identify this as the test browser
|
|
1407
|
+
await page.evaluate(() => {
|
|
1408
|
+
// @ts-ignore - browser context
|
|
1409
|
+
document.title = '🧪 TEST BROWSER - Console Logs Captured';
|
|
1410
|
+
});
|
|
1411
|
+
// Set up console event handler with student-friendly features
|
|
1412
|
+
const highlightErrors = validatedArgs.highlightErrors !== false;
|
|
1413
|
+
page.on('console', (msg) => {
|
|
1414
|
+
const msgType = msg.type();
|
|
1415
|
+
const timestamp = new Date().toISOString();
|
|
1416
|
+
const args = msg.args();
|
|
1417
|
+
// Format console message
|
|
1418
|
+
let messageText = msg.text();
|
|
1419
|
+
// Visual feedback for errors in teaching mode
|
|
1420
|
+
if (highlightErrors && msgType === 'error') {
|
|
1421
|
+
page.evaluate(() => {
|
|
1422
|
+
// @ts-ignore - browser context
|
|
1423
|
+
const flash = document.createElement('div');
|
|
1424
|
+
flash.style.cssText = `
|
|
1425
|
+
position: fixed;
|
|
1426
|
+
top: 0;
|
|
1427
|
+
left: 0;
|
|
1428
|
+
right: 0;
|
|
1429
|
+
bottom: 0;
|
|
1430
|
+
border: 5px solid red;
|
|
1431
|
+
pointer-events: none;
|
|
1432
|
+
z-index: 999999;
|
|
1433
|
+
animation: flash 0.5s ease-in-out;
|
|
1434
|
+
`;
|
|
1435
|
+
flash.innerHTML = `
|
|
1436
|
+
<div style="
|
|
1437
|
+
background: red;
|
|
1438
|
+
color: white;
|
|
1439
|
+
padding: 10px;
|
|
1440
|
+
position: absolute;
|
|
1441
|
+
top: 0;
|
|
1442
|
+
left: 0;
|
|
1443
|
+
right: 0;
|
|
1444
|
+
text-align: center;
|
|
1445
|
+
font-family: monospace;
|
|
1446
|
+
">⚠️ Console Error Detected - Check Logs!</div>
|
|
1447
|
+
`;
|
|
1448
|
+
// @ts-ignore - browser context
|
|
1449
|
+
document.body.appendChild(flash);
|
|
1450
|
+
setTimeout(() => flash.remove(), 2000);
|
|
1451
|
+
}).catch(() => { });
|
|
1452
|
+
}
|
|
1453
|
+
// Try to get better formatting for objects
|
|
1454
|
+
if (args.length > 0) {
|
|
1455
|
+
Promise.all(args.map(arg => arg.jsonValue().catch(() => arg.toString())))
|
|
1456
|
+
.then(values => {
|
|
1457
|
+
const formattedMsg = values.map(v => typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v)).join(' ');
|
|
1458
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${formattedMsg}\n`;
|
|
1459
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1460
|
+
// Also log to terminal for immediate feedback
|
|
1461
|
+
if (msgType === 'error' || msgType === 'warning') {
|
|
1462
|
+
console.error(`[STUDENT BROWSER] ${msgType === 'warning' ? 'WARN' : msgType.toUpperCase()}: ${formattedMsg}`);
|
|
1463
|
+
}
|
|
1464
|
+
})
|
|
1465
|
+
.catch(() => {
|
|
1466
|
+
// Fallback to simple text
|
|
1467
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
|
|
1468
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType.toUpperCase()}] ${messageText}\n`;
|
|
1473
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
// Handle page errors (including hydration errors)
|
|
1477
|
+
page.on('pageerror', (error) => {
|
|
1478
|
+
const timestamp = new Date().toISOString();
|
|
1479
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
|
|
1480
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1481
|
+
// Visual feedback for page errors
|
|
1482
|
+
page.evaluate(() => {
|
|
1483
|
+
// @ts-ignore - browser context
|
|
1484
|
+
const errorBanner = document.createElement('div');
|
|
1485
|
+
errorBanner.style.cssText = `
|
|
1486
|
+
position: fixed;
|
|
1487
|
+
top: 40px;
|
|
1488
|
+
left: 50%;
|
|
1489
|
+
transform: translateX(-50%);
|
|
1490
|
+
background: #dc2626;
|
|
1491
|
+
color: white;
|
|
1492
|
+
padding: 12px 24px;
|
|
1493
|
+
border-radius: 6px;
|
|
1494
|
+
font-family: monospace;
|
|
1495
|
+
font-size: 14px;
|
|
1496
|
+
z-index: 999999;
|
|
1497
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
|
1498
|
+
animation: slideDown 0.3s ease-out;
|
|
1499
|
+
`;
|
|
1500
|
+
errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
|
|
1501
|
+
// @ts-ignore - browser context
|
|
1502
|
+
const style = document.createElement('style');
|
|
1503
|
+
style.textContent = `
|
|
1504
|
+
@keyframes slideDown {
|
|
1505
|
+
from { transform: translate(-50%, -100%); opacity: 0; }
|
|
1506
|
+
to { transform: translate(-50%, 0); opacity: 1; }
|
|
1507
|
+
}
|
|
1508
|
+
`;
|
|
1509
|
+
// @ts-ignore - browser context
|
|
1510
|
+
document.head.appendChild(style);
|
|
1511
|
+
// @ts-ignore - browser context
|
|
1512
|
+
document.body.appendChild(errorBanner);
|
|
1513
|
+
setTimeout(() => errorBanner.remove(), 5000);
|
|
1514
|
+
}).catch(() => { });
|
|
1515
|
+
});
|
|
1516
|
+
// Handle unhandled promise rejections
|
|
1517
|
+
await page.addInitScript(() => {
|
|
1518
|
+
// @ts-ignore - browser context
|
|
1519
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1520
|
+
console.error('Unhandled Promise Rejection:', event.reason);
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
// Handle network response errors
|
|
1524
|
+
page.on('response', (response) => {
|
|
1525
|
+
if (response.status() >= 400) {
|
|
1526
|
+
const timestamp = new Date().toISOString();
|
|
1527
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
|
|
1528
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1529
|
+
}
|
|
1530
|
+
});
|
|
1531
|
+
// Navigate to URL
|
|
1532
|
+
const browserUrl = validatedArgs.browserUrl || "http://localhost:3000";
|
|
1533
|
+
try {
|
|
1534
|
+
await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
|
|
1535
|
+
// Add student instruction banner
|
|
1536
|
+
if (teachingMode) {
|
|
1537
|
+
await page.evaluate(() => {
|
|
1538
|
+
// @ts-ignore - browser context
|
|
1539
|
+
const banner = document.createElement('div');
|
|
1540
|
+
banner.id = 'test-browser-banner';
|
|
1541
|
+
banner.style.cssText = `
|
|
1542
|
+
position: fixed;
|
|
1543
|
+
top: 0;
|
|
1544
|
+
left: 0;
|
|
1545
|
+
right: 0;
|
|
1546
|
+
background: #4CAF50;
|
|
1547
|
+
color: white;
|
|
1548
|
+
padding: 10px;
|
|
1549
|
+
text-align: center;
|
|
1550
|
+
z-index: 99999;
|
|
1551
|
+
font-family: monospace;
|
|
1552
|
+
font-size: 14px;
|
|
1553
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
1554
|
+
`;
|
|
1555
|
+
banner.innerHTML = `
|
|
1556
|
+
🧪 TEST BROWSER - All console logs are being captured!
|
|
1557
|
+
<button onclick="this.parentElement.style.display='none'" style="
|
|
1558
|
+
margin-left: 20px;
|
|
1559
|
+
background: transparent;
|
|
1560
|
+
border: 1px solid white;
|
|
1561
|
+
color: white;
|
|
1562
|
+
padding: 2px 10px;
|
|
1563
|
+
cursor: pointer;
|
|
1564
|
+
">Hide</button>
|
|
1565
|
+
`;
|
|
1566
|
+
// @ts-ignore - browser context
|
|
1567
|
+
document.body.prepend(banner);
|
|
1568
|
+
}).catch(() => { });
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
catch (error) {
|
|
1572
|
+
const timestamp = new Date().toISOString();
|
|
1573
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NAVIGATION ERROR] Failed to navigate to ${browserUrl}: ${error}\n`;
|
|
1574
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1575
|
+
}
|
|
1576
|
+
// Update server info
|
|
1577
|
+
serverInfo.browser = browser;
|
|
1578
|
+
serverInfo.page = page;
|
|
1579
|
+
serverInfo.browserUrl = browserUrl;
|
|
1580
|
+
serverInfo.consoleCapture = true;
|
|
1581
|
+
// Log browser start
|
|
1582
|
+
const timestamp = new Date().toISOString();
|
|
1583
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`;
|
|
1584
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1585
|
+
return {
|
|
1586
|
+
content: [
|
|
1587
|
+
{
|
|
1588
|
+
type: "text",
|
|
1589
|
+
text: JSON.stringify({
|
|
1590
|
+
status: "started",
|
|
1591
|
+
processId: processId,
|
|
1592
|
+
browserUrl: browserUrl,
|
|
1593
|
+
teachingMode: teachingMode,
|
|
1594
|
+
message: `🧪 TEST BROWSER LAUNCHED!\n\n` +
|
|
1595
|
+
`👉 IMPORTANT: Students should use THIS browser window (not their regular browser)\n` +
|
|
1596
|
+
`📝 All console logs are being captured to: ${serverInfo.outputFile}\n` +
|
|
1597
|
+
`🔍 DevTools is ${teachingMode ? 'OPEN' : 'CLOSED'} for debugging\n` +
|
|
1598
|
+
`⚠️ Errors will ${highlightErrors ? 'flash red' : 'not flash'} on screen\n\n` +
|
|
1599
|
+
`Students can now interact with the app at: ${browserUrl}`
|
|
1600
|
+
}, null, 2)
|
|
1601
|
+
}
|
|
1602
|
+
]
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
throw new Error(`Failed to start browser console capture: ${error instanceof Error ? error.message : String(error)}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
case "dev_stop_browser_console": {
|
|
1610
|
+
const validatedArgs = z.object({
|
|
1611
|
+
processId: z.string().optional()
|
|
1612
|
+
}).parse(args);
|
|
1613
|
+
try {
|
|
1614
|
+
const processId = this.findProcessOrDefault(validatedArgs.processId);
|
|
1615
|
+
if (!processId) {
|
|
1616
|
+
return {
|
|
1617
|
+
content: [
|
|
1618
|
+
{
|
|
1619
|
+
type: "text",
|
|
1620
|
+
text: JSON.stringify({
|
|
1621
|
+
status: "error",
|
|
1622
|
+
message: "No process found"
|
|
1623
|
+
}, null, 2)
|
|
1624
|
+
}
|
|
1625
|
+
]
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
const serverInfo = this.activeServers.get(processId);
|
|
1629
|
+
if (!serverInfo || !serverInfo.browser) {
|
|
1630
|
+
return {
|
|
1631
|
+
content: [
|
|
1632
|
+
{
|
|
1633
|
+
type: "text",
|
|
1634
|
+
text: JSON.stringify({
|
|
1635
|
+
status: "not_running",
|
|
1636
|
+
message: `No browser console capture running for process '${processId}'`
|
|
1637
|
+
}, null, 2)
|
|
1638
|
+
}
|
|
1639
|
+
]
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
// Close browser
|
|
1643
|
+
await serverInfo.browser.close();
|
|
1644
|
+
// Clear browser info
|
|
1645
|
+
serverInfo.browser = undefined;
|
|
1646
|
+
serverInfo.page = undefined;
|
|
1647
|
+
serverInfo.browserUrl = undefined;
|
|
1648
|
+
serverInfo.consoleCapture = false;
|
|
1649
|
+
// Log browser stop
|
|
1650
|
+
const timestamp = new Date().toISOString();
|
|
1651
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture stopped\n`;
|
|
1652
|
+
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1653
|
+
return {
|
|
1654
|
+
content: [
|
|
1655
|
+
{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: JSON.stringify({
|
|
1658
|
+
status: "stopped",
|
|
1659
|
+
processId: processId,
|
|
1660
|
+
message: `Browser console capture stopped for process '${processId}'`
|
|
1661
|
+
}, null, 2)
|
|
1662
|
+
}
|
|
1663
|
+
]
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
catch (error) {
|
|
1667
|
+
throw new Error(`Failed to stop browser console capture: ${error instanceof Error ? error.message : String(error)}`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
case "dev_start_frontend_with_browser": {
|
|
1671
|
+
const validatedArgs = StartFrontendWithBrowserArgsSchema.parse(args);
|
|
1672
|
+
// Defaults
|
|
1673
|
+
const command = validatedArgs.command || "npm run dev";
|
|
1674
|
+
const processId = validatedArgs.processId || "frontend-with-browser";
|
|
1675
|
+
const cwd = resolve(validatedArgs.cwd || process.cwd());
|
|
1676
|
+
const waitTimeout = validatedArgs.waitTimeout || 30000;
|
|
1677
|
+
const browserDelay = validatedArgs.browserDelay || 1000;
|
|
1678
|
+
const teachingMode = validatedArgs.teachingMode !== false; // default true
|
|
1679
|
+
try {
|
|
1680
|
+
// Check if process already exists
|
|
1681
|
+
if (this.activeServers.has(processId)) {
|
|
1682
|
+
throw new Error(`Process '${processId}' is already running. Stop it first or use a different processId.`);
|
|
1683
|
+
}
|
|
1684
|
+
// Step 1: Start the dev server
|
|
1685
|
+
const outputFile = this.createStructuredLogPath(command, undefined, "frontend");
|
|
1686
|
+
this.cleanupOldLogs(3);
|
|
1687
|
+
const [program, ...cmdArgs] = command.split(' ');
|
|
1688
|
+
// Initial log entry
|
|
1689
|
+
writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
|
|
1690
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Starting development server...\n`);
|
|
1691
|
+
// Spawn the process
|
|
1692
|
+
const devProcess = spawn(program, cmdArgs, {
|
|
1693
|
+
cwd: cwd,
|
|
1694
|
+
env: { ...process.env, ...validatedArgs.env },
|
|
1695
|
+
detached: true,
|
|
1696
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1697
|
+
});
|
|
1698
|
+
const serverInfo = {
|
|
1699
|
+
process: devProcess,
|
|
1700
|
+
command: command,
|
|
1701
|
+
cwd: cwd,
|
|
1702
|
+
outputFile: outputFile,
|
|
1703
|
+
startTime: new Date(),
|
|
1704
|
+
pid: devProcess.pid,
|
|
1705
|
+
processId: processId
|
|
1706
|
+
};
|
|
1707
|
+
this.activeServers.set(processId, serverInfo);
|
|
1708
|
+
// Variables for port detection
|
|
1709
|
+
let detectedPort = validatedArgs.port || null;
|
|
1710
|
+
let serverReady = false;
|
|
1711
|
+
let outputBuffer = "";
|
|
1712
|
+
// Common port patterns
|
|
1713
|
+
const portPatterns = [
|
|
1714
|
+
/(?:Local|Listening|ready).*?(\d{4})/i,
|
|
1715
|
+
/localhost:(\d{4})/,
|
|
1716
|
+
/port\s*:?\s*(\d{4})/i,
|
|
1717
|
+
/on\s+http:\/\/localhost:(\d{4})/i,
|
|
1718
|
+
/Server running at.*?:(\d{4})/i
|
|
1719
|
+
];
|
|
1720
|
+
// Ready indicators
|
|
1721
|
+
const readyPatterns = [
|
|
1722
|
+
/Local:\s*http:\/\/localhost/i,
|
|
1723
|
+
/ready on http:\/\/localhost/i,
|
|
1724
|
+
/Server running at/i,
|
|
1725
|
+
/Listening on port/i,
|
|
1726
|
+
/started server on/i,
|
|
1727
|
+
/Ready in \d+ms/i,
|
|
1728
|
+
/compiled successfully/i
|
|
1729
|
+
];
|
|
1730
|
+
// Create promise for server ready detection
|
|
1731
|
+
const serverReadyPromise = new Promise((resolve, reject) => {
|
|
1732
|
+
const timeout = setTimeout(() => {
|
|
1733
|
+
reject(new Error(`Server did not start within ${waitTimeout}ms`));
|
|
1734
|
+
}, waitTimeout);
|
|
1735
|
+
const checkOutput = (data) => {
|
|
1736
|
+
const text = data.toString();
|
|
1737
|
+
outputBuffer += text;
|
|
1738
|
+
// Check for port if not detected
|
|
1739
|
+
if (!detectedPort) {
|
|
1740
|
+
for (const pattern of portPatterns) {
|
|
1741
|
+
const match = outputBuffer.match(pattern);
|
|
1742
|
+
if (match && match[1]) {
|
|
1743
|
+
detectedPort = parseInt(match[1]);
|
|
1744
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] 🔍 Detected port: ${detectedPort}\n`);
|
|
1745
|
+
break;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
// Check for ready state
|
|
1750
|
+
if (!serverReady) {
|
|
1751
|
+
for (const pattern of readyPatterns) {
|
|
1752
|
+
if (pattern.test(outputBuffer)) {
|
|
1753
|
+
serverReady = true;
|
|
1754
|
+
clearTimeout(timeout);
|
|
1755
|
+
// If we have a port, resolve immediately
|
|
1756
|
+
if (detectedPort) {
|
|
1757
|
+
resolve(detectedPort);
|
|
1758
|
+
}
|
|
1759
|
+
else {
|
|
1760
|
+
// Try common ports
|
|
1761
|
+
const commonPorts = [3000, 3001, 5173, 8080, 4200];
|
|
1762
|
+
detectedPort = commonPorts[0]; // Default to 3000
|
|
1763
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⚠️ Could not detect port, defaulting to ${detectedPort}\n`);
|
|
1764
|
+
resolve(detectedPort);
|
|
1765
|
+
}
|
|
1766
|
+
break;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1771
|
+
// Attach listeners
|
|
1772
|
+
devProcess.stdout?.on('data', checkOutput);
|
|
1773
|
+
devProcess.stderr?.on('data', checkOutput);
|
|
1774
|
+
});
|
|
1775
|
+
// Stream stdout to file
|
|
1776
|
+
devProcess.stdout?.on('data', (data) => {
|
|
1777
|
+
const timestamp = new Date().toISOString();
|
|
1778
|
+
const logEntry = `[${timestamp}] [${processId}] ${data}`;
|
|
1779
|
+
appendFileSync(outputFile, logEntry);
|
|
1780
|
+
});
|
|
1781
|
+
// Stream stderr to file
|
|
1782
|
+
devProcess.stderr?.on('data', (data) => {
|
|
1783
|
+
const timestamp = new Date().toISOString();
|
|
1784
|
+
const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
|
|
1785
|
+
appendFileSync(outputFile, logEntry);
|
|
1786
|
+
});
|
|
1787
|
+
// Handle process exit
|
|
1788
|
+
devProcess.on('exit', (code, signal) => {
|
|
1789
|
+
const timestamp = new Date().toISOString();
|
|
1790
|
+
const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
|
|
1791
|
+
appendFileSync(outputFile, exitMessage);
|
|
1792
|
+
this.activeServers.delete(processId);
|
|
1793
|
+
this.saveState();
|
|
1794
|
+
});
|
|
1795
|
+
// Handle process errors
|
|
1796
|
+
devProcess.on('error', (error) => {
|
|
1797
|
+
const timestamp = new Date().toISOString();
|
|
1798
|
+
const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
|
|
1799
|
+
appendFileSync(outputFile, errorMessage);
|
|
1800
|
+
});
|
|
1801
|
+
this.saveState();
|
|
1802
|
+
// Step 2: Wait for server to be ready
|
|
1803
|
+
let port;
|
|
1804
|
+
try {
|
|
1805
|
+
port = await serverReadyPromise;
|
|
1806
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Server ready on http://localhost:${port}\n`);
|
|
1807
|
+
}
|
|
1808
|
+
catch (error) {
|
|
1809
|
+
// Server didn't start properly, clean up
|
|
1810
|
+
const failedServerInfo = this.activeServers.get(processId);
|
|
1811
|
+
if (failedServerInfo) {
|
|
1812
|
+
try {
|
|
1813
|
+
// Close browser if it exists
|
|
1814
|
+
if (failedServerInfo.browser) {
|
|
1815
|
+
await failedServerInfo.browser.close();
|
|
1816
|
+
}
|
|
1817
|
+
// Remove all event listeners to prevent memory leaks
|
|
1818
|
+
if (failedServerInfo.process) {
|
|
1819
|
+
failedServerInfo.process.stdout?.removeAllListeners('data');
|
|
1820
|
+
failedServerInfo.process.stderr?.removeAllListeners('data');
|
|
1821
|
+
failedServerInfo.process.removeAllListeners('exit');
|
|
1822
|
+
failedServerInfo.process.removeAllListeners('error');
|
|
1823
|
+
}
|
|
1824
|
+
// Kill the process
|
|
1825
|
+
if (failedServerInfo.pid) {
|
|
1826
|
+
process.kill(failedServerInfo.pid, 'SIGTERM');
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
catch (killError) {
|
|
1830
|
+
// Ignore errors during cleanup
|
|
1831
|
+
}
|
|
1832
|
+
this.activeServers.delete(processId);
|
|
1833
|
+
this.saveState();
|
|
1834
|
+
}
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
// Step 3: Wait a bit more for stability
|
|
1838
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⏳ Waiting ${browserDelay}ms before launching browser...\n`);
|
|
1839
|
+
await new Promise(resolve => setTimeout(resolve, browserDelay));
|
|
1840
|
+
// Step 4: Launch the test browser
|
|
1841
|
+
try {
|
|
1842
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Launching test browser...\n`);
|
|
1843
|
+
const browser = await chromium.launch({
|
|
1844
|
+
headless: false,
|
|
1845
|
+
slowMo: teachingMode ? 50 : 0,
|
|
1846
|
+
devtools: teachingMode,
|
|
1847
|
+
args: ['--window-size=1280,800', '--window-position=100,100']
|
|
1848
|
+
});
|
|
1849
|
+
const context = await browser.newContext({
|
|
1850
|
+
viewport: { width: 1280, height: 800 }
|
|
1851
|
+
});
|
|
1852
|
+
const page = await context.newPage();
|
|
1853
|
+
// Add banner to identify test browser
|
|
1854
|
+
await page.addInitScript(() => {
|
|
1855
|
+
// @ts-ignore - browser context
|
|
1856
|
+
const banner = document.createElement('div');
|
|
1857
|
+
banner.id = 'test-browser-banner';
|
|
1858
|
+
banner.innerHTML = '🧪 TEST BROWSER - All console logs are being captured 📝';
|
|
1859
|
+
banner.style.cssText = `
|
|
1860
|
+
position: fixed;
|
|
1861
|
+
top: 0;
|
|
1862
|
+
left: 0;
|
|
1863
|
+
right: 0;
|
|
1864
|
+
background: #22c55e;
|
|
1865
|
+
color: white;
|
|
1866
|
+
padding: 8px;
|
|
1867
|
+
text-align: center;
|
|
1868
|
+
font-family: monospace;
|
|
1869
|
+
font-size: 14px;
|
|
1870
|
+
z-index: 999999;
|
|
1871
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
1872
|
+
`;
|
|
1873
|
+
// @ts-ignore - browser context
|
|
1874
|
+
if (document.readyState === 'loading') {
|
|
1875
|
+
// @ts-ignore - browser context
|
|
1876
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1877
|
+
// @ts-ignore - browser context
|
|
1878
|
+
document.body.insertBefore(banner, document.body.firstChild);
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
else {
|
|
1882
|
+
// @ts-ignore - browser context
|
|
1883
|
+
document.body.insertBefore(banner, document.body.firstChild);
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
// Set up console capture
|
|
1887
|
+
page.on('console', (msg) => {
|
|
1888
|
+
const timestamp = new Date().toISOString();
|
|
1889
|
+
const msgType = msg.type().toUpperCase();
|
|
1890
|
+
const msgText = msg.text();
|
|
1891
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType}] ${msgText}\n`;
|
|
1892
|
+
appendFileSync(outputFile, logEntry);
|
|
1893
|
+
// Flash red on errors
|
|
1894
|
+
if (msgType === 'ERROR') {
|
|
1895
|
+
page.evaluate(() => {
|
|
1896
|
+
// @ts-ignore - browser context
|
|
1897
|
+
const flash = document.createElement('div');
|
|
1898
|
+
flash.style.cssText = `
|
|
1899
|
+
position: fixed;
|
|
1900
|
+
top: 0;
|
|
1901
|
+
left: 0;
|
|
1902
|
+
right: 0;
|
|
1903
|
+
bottom: 0;
|
|
1904
|
+
background: rgba(239, 68, 68, 0.3);
|
|
1905
|
+
z-index: 999998;
|
|
1906
|
+
pointer-events: none;
|
|
1907
|
+
animation: errorFlash 0.5s ease-out;
|
|
1908
|
+
`;
|
|
1909
|
+
// @ts-ignore - browser context
|
|
1910
|
+
const style = document.createElement('style');
|
|
1911
|
+
style.textContent = `
|
|
1912
|
+
@keyframes errorFlash {
|
|
1913
|
+
0% { opacity: 1; }
|
|
1914
|
+
100% { opacity: 0; }
|
|
1915
|
+
}
|
|
1916
|
+
`;
|
|
1917
|
+
// @ts-ignore - browser context
|
|
1918
|
+
document.head.appendChild(style);
|
|
1919
|
+
// @ts-ignore - browser context
|
|
1920
|
+
document.body.appendChild(flash);
|
|
1921
|
+
setTimeout(() => flash.remove(), 500);
|
|
1922
|
+
}).catch(() => { }); // Ignore errors from flashing
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
// Handle page errors (including hydration errors)
|
|
1926
|
+
page.on('pageerror', (error) => {
|
|
1927
|
+
const timestamp = new Date().toISOString();
|
|
1928
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
|
|
1929
|
+
appendFileSync(outputFile, logEntry);
|
|
1930
|
+
// Visual feedback for page errors
|
|
1931
|
+
page.evaluate(() => {
|
|
1932
|
+
// @ts-ignore - browser context
|
|
1933
|
+
const errorBanner = document.createElement('div');
|
|
1934
|
+
errorBanner.style.cssText = `
|
|
1935
|
+
position: fixed;
|
|
1936
|
+
top: 40px;
|
|
1937
|
+
left: 50%;
|
|
1938
|
+
transform: translateX(-50%);
|
|
1939
|
+
background: #dc2626;
|
|
1940
|
+
color: white;
|
|
1941
|
+
padding: 12px 24px;
|
|
1942
|
+
border-radius: 6px;
|
|
1943
|
+
font-family: monospace;
|
|
1944
|
+
font-size: 14px;
|
|
1945
|
+
z-index: 999999;
|
|
1946
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
|
1947
|
+
animation: slideDown 0.3s ease-out;
|
|
1948
|
+
`;
|
|
1949
|
+
errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
|
|
1950
|
+
// @ts-ignore - browser context
|
|
1951
|
+
const style = document.createElement('style');
|
|
1952
|
+
style.textContent = `
|
|
1953
|
+
@keyframes slideDown {
|
|
1954
|
+
from { transform: translate(-50%, -100%); opacity: 0; }
|
|
1955
|
+
to { transform: translate(-50%, 0); opacity: 1; }
|
|
1956
|
+
}
|
|
1957
|
+
`;
|
|
1958
|
+
// @ts-ignore - browser context
|
|
1959
|
+
document.head.appendChild(style);
|
|
1960
|
+
// @ts-ignore - browser context
|
|
1961
|
+
document.body.appendChild(errorBanner);
|
|
1962
|
+
setTimeout(() => errorBanner.remove(), 5000);
|
|
1963
|
+
}).catch(() => { });
|
|
1964
|
+
});
|
|
1965
|
+
// Handle unhandled promise rejections
|
|
1966
|
+
await page.addInitScript(() => {
|
|
1967
|
+
// @ts-ignore - browser context
|
|
1968
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
1969
|
+
console.error('Unhandled Promise Rejection:', event.reason);
|
|
1970
|
+
});
|
|
1971
|
+
});
|
|
1972
|
+
// Handle network response errors
|
|
1973
|
+
page.on('response', (response) => {
|
|
1974
|
+
if (response.status() >= 400) {
|
|
1975
|
+
const timestamp = new Date().toISOString();
|
|
1976
|
+
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
|
|
1977
|
+
appendFileSync(outputFile, logEntry);
|
|
1978
|
+
}
|
|
1979
|
+
});
|
|
1980
|
+
// Navigate to the app
|
|
1981
|
+
const browserUrl = `http://localhost:${port}`;
|
|
1982
|
+
await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
|
|
1983
|
+
// Update server info with browser details
|
|
1984
|
+
serverInfo.browser = browser;
|
|
1985
|
+
serverInfo.page = page;
|
|
1986
|
+
serverInfo.browserUrl = browserUrl;
|
|
1987
|
+
serverInfo.consoleCapture = true;
|
|
1988
|
+
// Log success
|
|
1989
|
+
const successMessage = `
|
|
1990
|
+
🎯 TEST BROWSER LAUNCHED!
|
|
1991
|
+
|
|
1992
|
+
👉 Use the browser window that just opened (with green banner)
|
|
1993
|
+
📝 All console logs are being saved
|
|
1994
|
+
🔍 DevTools is open for debugging
|
|
1995
|
+
⚠️ Errors will flash red on screen
|
|
1996
|
+
|
|
1997
|
+
Happy debugging! 🚀
|
|
1998
|
+
`;
|
|
1999
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`);
|
|
2000
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ${successMessage}\n`);
|
|
2001
|
+
return {
|
|
2002
|
+
content: [
|
|
2003
|
+
{
|
|
2004
|
+
type: "text",
|
|
2005
|
+
text: JSON.stringify({
|
|
2006
|
+
status: "success",
|
|
2007
|
+
message: successMessage.trim(),
|
|
2008
|
+
processId: processId,
|
|
2009
|
+
serverPid: devProcess.pid,
|
|
2010
|
+
serverUrl: `http://localhost:${port}`,
|
|
2011
|
+
logFile: outputFile,
|
|
2012
|
+
browserStatus: "launched",
|
|
2013
|
+
teachingMode: teachingMode
|
|
2014
|
+
}, null, 2)
|
|
2015
|
+
}
|
|
2016
|
+
]
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
catch (browserError) {
|
|
2020
|
+
// Browser launch failed, but server is running
|
|
2021
|
+
const errorMsg = browserError instanceof Error ? browserError.message : String(browserError);
|
|
2022
|
+
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [ERROR] Failed to launch browser: ${errorMsg}\n`);
|
|
2023
|
+
return {
|
|
2024
|
+
content: [
|
|
2025
|
+
{
|
|
2026
|
+
type: "text",
|
|
2027
|
+
text: JSON.stringify({
|
|
2028
|
+
status: "partial_success",
|
|
2029
|
+
message: `Server started successfully on http://localhost:${port}, but browser launch failed`,
|
|
2030
|
+
error: errorMsg,
|
|
2031
|
+
processId: processId,
|
|
2032
|
+
serverPid: devProcess.pid,
|
|
2033
|
+
serverUrl: `http://localhost:${port}`,
|
|
2034
|
+
logFile: outputFile,
|
|
2035
|
+
browserStatus: "failed",
|
|
2036
|
+
manualInstructions: "Open http://localhost:" + port + " in your browser manually"
|
|
2037
|
+
}, null, 2)
|
|
2038
|
+
}
|
|
2039
|
+
]
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
catch (error) {
|
|
2044
|
+
throw new Error(`Failed to start frontend with browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1236
2047
|
default:
|
|
1237
2048
|
throw new Error(`Unknown tool: ${name}`);
|
|
1238
2049
|
}
|