@chinchillaenterprises/mcp-dev-logger 2.0.2 โ†’ 2.3.0

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/index.js CHANGED
@@ -4,10 +4,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
6
  import { spawn, exec } from "child_process";
7
- import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync, readdirSync, statSync, mkdirSync } from "fs";
7
+ import { writeFileSync, readFileSync, existsSync, appendFileSync, copyFileSync, readdirSync, statSync, mkdirSync, rmSync } from "fs";
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;
@@ -348,41 +359,46 @@ class DevLoggerServer {
348
359
  if (!existsSync(sessionDir)) {
349
360
  mkdirSync(sessionDir, { recursive: true });
350
361
  }
351
- // Determine log type and generate standardized filename
362
+ // Determine log type and create process type subfolder
352
363
  const detectedType = logType || this.detectProcessType(command);
364
+ const processTypeDir = join(sessionDir, detectedType);
365
+ if (!existsSync(processTypeDir)) {
366
+ mkdirSync(processTypeDir, { recursive: true });
367
+ }
368
+ // Generate standardized filename
353
369
  const standardizedName = this.generateStandardizedLogName(detectedType, command);
354
- return join(sessionDir, standardizedName);
370
+ return join(processTypeDir, standardizedName);
355
371
  }
356
372
  generateStandardizedLogName(logType, command) {
357
- // Standardized naming conventions to prevent confusion
373
+ // Standardized naming conventions without redundant prefixes
358
374
  const timestamp = new Date().toTimeString().split(' ')[0].replace(/:/g, '-');
359
375
  switch (logType) {
360
376
  case 'frontend':
361
377
  if (command.includes('next'))
362
- return `frontend-nextjs-${timestamp}.log`;
378
+ return `nextjs-${timestamp}.log`;
363
379
  if (command.includes('vite'))
364
- return `frontend-vite-${timestamp}.log`;
380
+ return `vite-${timestamp}.log`;
365
381
  if (command.includes('react'))
366
- return `frontend-react-${timestamp}.log`;
367
- return `frontend-dev-${timestamp}.log`;
382
+ return `react-${timestamp}.log`;
383
+ return `dev-${timestamp}.log`;
368
384
  case 'backend':
369
385
  if (command.includes('express'))
370
- return `backend-express-${timestamp}.log`;
386
+ return `express-${timestamp}.log`;
371
387
  if (command.includes('fastify'))
372
- return `backend-fastify-${timestamp}.log`;
388
+ return `fastify-${timestamp}.log`;
373
389
  if (command.includes('node'))
374
- return `backend-node-${timestamp}.log`;
375
- return `backend-api-${timestamp}.log`;
390
+ return `node-${timestamp}.log`;
391
+ return `api-${timestamp}.log`;
376
392
  case 'amplify':
377
393
  if (command.includes('sandbox'))
378
- return `amplify-sandbox-${timestamp}.log`;
394
+ return `sandbox-${timestamp}.log`;
379
395
  if (command.includes('deploy'))
380
- return `amplify-deploy-${timestamp}.log`;
381
- return `amplify-dev-${timestamp}.log`;
396
+ return `deploy-${timestamp}.log`;
397
+ return `dev-${timestamp}.log`;
382
398
  default:
383
399
  // Extract first word of command for custom processes
384
400
  const cmdName = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
385
- return `custom-${cmdName}-${timestamp}.log`;
401
+ return `${cmdName}-${timestamp}.log`;
386
402
  }
387
403
  }
388
404
  shouldUseStructuredLogging(args) {
@@ -394,9 +410,52 @@ class DevLoggerServer {
394
410
  return false;
395
411
  return true;
396
412
  }
397
- killAllProcesses() {
413
+ cleanupOldLogs(retentionDays = 3) {
414
+ try {
415
+ const cwd = process.cwd();
416
+ const logsDir = join(cwd, 'logs');
417
+ // Skip if logs directory doesn't exist
418
+ if (!existsSync(logsDir)) {
419
+ return;
420
+ }
421
+ const now = new Date();
422
+ const cutoffTime = now.getTime() - (retentionDays * 24 * 60 * 60 * 1000);
423
+ // Read all entries in logs directory
424
+ const entries = readdirSync(logsDir);
425
+ for (const entry of entries) {
426
+ // Check if it's a date folder (YYYY-MM-DD format)
427
+ if (/^\d{4}-\d{2}-\d{2}$/.test(entry)) {
428
+ const entryPath = join(logsDir, entry);
429
+ const stats = statSync(entryPath);
430
+ if (stats.isDirectory()) {
431
+ // Parse the date from folder name
432
+ const folderDate = new Date(entry + 'T00:00:00');
433
+ // If folder is older than retention period, remove it
434
+ if (folderDate.getTime() < cutoffTime) {
435
+ try {
436
+ rmSync(entryPath, { recursive: true, force: true });
437
+ console.error(`Cleaned up old log directory: ${entry}`);
438
+ }
439
+ catch (error) {
440
+ console.error(`Failed to remove old log directory ${entry}:`, error);
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ catch (error) {
448
+ // Log cleanup failures shouldn't break the main functionality
449
+ console.error('Log cleanup error:', error);
450
+ }
451
+ }
452
+ async killAllProcesses() {
398
453
  for (const [id, info] of this.activeServers) {
399
454
  try {
455
+ // Close browser if running
456
+ if (info.browser) {
457
+ await info.browser.close();
458
+ }
400
459
  if (info.process) {
401
460
  // Remove all event listeners to prevent memory leaks
402
461
  info.process.stdout?.removeAllListeners('data');
@@ -589,6 +648,93 @@ class DevLoggerServer {
589
648
  }
590
649
  }
591
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
+ }
592
738
  }
593
739
  ];
594
740
  return { tools };
@@ -605,6 +751,8 @@ class DevLoggerServer {
605
751
  let outputFile;
606
752
  if (this.shouldUseStructuredLogging(validatedArgs)) {
607
753
  outputFile = this.createStructuredLogPath(command, validatedArgs.sessionDate, validatedArgs.logType);
754
+ // Clean up old logs when using structured logging (keeps last 3 days)
755
+ this.cleanupOldLogs(3);
608
756
  }
609
757
  else {
610
758
  outputFile = resolve(validatedArgs.outputFile || "dev-server-logs.txt");
@@ -797,6 +945,10 @@ class DevLoggerServer {
797
945
  };
798
946
  }
799
947
  try {
948
+ // Close browser if running
949
+ if (serverInfo.browser) {
950
+ await serverInfo.browser.close();
951
+ }
800
952
  // Remove all event listeners to prevent memory leaks
801
953
  if (serverInfo.process) {
802
954
  serverInfo.process.stdout?.removeAllListeners('data');
@@ -844,6 +996,10 @@ class DevLoggerServer {
844
996
  const stoppedProcesses = [];
845
997
  for (const [processId, serverInfo] of this.activeServers) {
846
998
  try {
999
+ // Close browser if running
1000
+ if (serverInfo.browser) {
1001
+ await serverInfo.browser.close();
1002
+ }
847
1003
  // Remove all event listeners to prevent memory leaks
848
1004
  if (serverInfo.process) {
849
1005
  serverInfo.process.stdout?.removeAllListeners('data');
@@ -1187,6 +1343,603 @@ class DevLoggerServer {
1187
1343
  throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
1188
1344
  }
1189
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
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
+ });
1482
+ // Navigate to URL
1483
+ const browserUrl = validatedArgs.browserUrl || "http://localhost:3000";
1484
+ try {
1485
+ await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
1486
+ // Add student instruction banner
1487
+ if (teachingMode) {
1488
+ await page.evaluate(() => {
1489
+ // @ts-ignore - browser context
1490
+ const banner = document.createElement('div');
1491
+ banner.id = 'test-browser-banner';
1492
+ banner.style.cssText = `
1493
+ position: fixed;
1494
+ top: 0;
1495
+ left: 0;
1496
+ right: 0;
1497
+ background: #4CAF50;
1498
+ color: white;
1499
+ padding: 10px;
1500
+ text-align: center;
1501
+ z-index: 99999;
1502
+ font-family: monospace;
1503
+ font-size: 14px;
1504
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
1505
+ `;
1506
+ banner.innerHTML = `
1507
+ ๐Ÿงช TEST BROWSER - All console logs are being captured!
1508
+ <button onclick="this.parentElement.style.display='none'" style="
1509
+ margin-left: 20px;
1510
+ background: transparent;
1511
+ border: 1px solid white;
1512
+ color: white;
1513
+ padding: 2px 10px;
1514
+ cursor: pointer;
1515
+ ">Hide</button>
1516
+ `;
1517
+ // @ts-ignore - browser context
1518
+ document.body.prepend(banner);
1519
+ }).catch(() => { });
1520
+ }
1521
+ }
1522
+ catch (error) {
1523
+ const timestamp = new Date().toISOString();
1524
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NAVIGATION ERROR] Failed to navigate to ${browserUrl}: ${error}\n`;
1525
+ appendFileSync(serverInfo.outputFile, logEntry);
1526
+ }
1527
+ // Update server info
1528
+ serverInfo.browser = browser;
1529
+ serverInfo.page = page;
1530
+ serverInfo.browserUrl = browserUrl;
1531
+ serverInfo.consoleCapture = true;
1532
+ // Log browser start
1533
+ const timestamp = new Date().toISOString();
1534
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`;
1535
+ appendFileSync(serverInfo.outputFile, logEntry);
1536
+ return {
1537
+ content: [
1538
+ {
1539
+ type: "text",
1540
+ text: JSON.stringify({
1541
+ status: "started",
1542
+ processId: processId,
1543
+ browserUrl: browserUrl,
1544
+ teachingMode: teachingMode,
1545
+ message: `๐Ÿงช TEST BROWSER LAUNCHED!\n\n` +
1546
+ `๐Ÿ‘‰ IMPORTANT: Students should use THIS browser window (not their regular browser)\n` +
1547
+ `๐Ÿ“ All console logs are being captured to: ${serverInfo.outputFile}\n` +
1548
+ `๐Ÿ” DevTools is ${teachingMode ? 'OPEN' : 'CLOSED'} for debugging\n` +
1549
+ `โš ๏ธ Errors will ${highlightErrors ? 'flash red' : 'not flash'} on screen\n\n` +
1550
+ `Students can now interact with the app at: ${browserUrl}`
1551
+ }, null, 2)
1552
+ }
1553
+ ]
1554
+ };
1555
+ }
1556
+ catch (error) {
1557
+ throw new Error(`Failed to start browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1558
+ }
1559
+ }
1560
+ case "dev_stop_browser_console": {
1561
+ const validatedArgs = z.object({
1562
+ processId: z.string().optional()
1563
+ }).parse(args);
1564
+ try {
1565
+ const processId = this.findProcessOrDefault(validatedArgs.processId);
1566
+ if (!processId) {
1567
+ return {
1568
+ content: [
1569
+ {
1570
+ type: "text",
1571
+ text: JSON.stringify({
1572
+ status: "error",
1573
+ message: "No process found"
1574
+ }, null, 2)
1575
+ }
1576
+ ]
1577
+ };
1578
+ }
1579
+ const serverInfo = this.activeServers.get(processId);
1580
+ if (!serverInfo || !serverInfo.browser) {
1581
+ return {
1582
+ content: [
1583
+ {
1584
+ type: "text",
1585
+ text: JSON.stringify({
1586
+ status: "not_running",
1587
+ message: `No browser console capture running for process '${processId}'`
1588
+ }, null, 2)
1589
+ }
1590
+ ]
1591
+ };
1592
+ }
1593
+ // Close browser
1594
+ await serverInfo.browser.close();
1595
+ // Clear browser info
1596
+ serverInfo.browser = undefined;
1597
+ serverInfo.page = undefined;
1598
+ serverInfo.browserUrl = undefined;
1599
+ serverInfo.consoleCapture = false;
1600
+ // Log browser stop
1601
+ const timestamp = new Date().toISOString();
1602
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture stopped\n`;
1603
+ appendFileSync(serverInfo.outputFile, logEntry);
1604
+ return {
1605
+ content: [
1606
+ {
1607
+ type: "text",
1608
+ text: JSON.stringify({
1609
+ status: "stopped",
1610
+ processId: processId,
1611
+ message: `Browser console capture stopped for process '${processId}'`
1612
+ }, null, 2)
1613
+ }
1614
+ ]
1615
+ };
1616
+ }
1617
+ catch (error) {
1618
+ throw new Error(`Failed to stop browser console capture: ${error instanceof Error ? error.message : String(error)}`);
1619
+ }
1620
+ }
1621
+ case "dev_start_frontend_with_browser": {
1622
+ const validatedArgs = StartFrontendWithBrowserArgsSchema.parse(args);
1623
+ // Defaults
1624
+ const command = validatedArgs.command || "npm run dev";
1625
+ const processId = validatedArgs.processId || "frontend-with-browser";
1626
+ const cwd = resolve(validatedArgs.cwd || process.cwd());
1627
+ const waitTimeout = validatedArgs.waitTimeout || 30000;
1628
+ const browserDelay = validatedArgs.browserDelay || 1000;
1629
+ const teachingMode = validatedArgs.teachingMode !== false; // default true
1630
+ try {
1631
+ // Check if process already exists
1632
+ if (this.activeServers.has(processId)) {
1633
+ throw new Error(`Process '${processId}' is already running. Stop it first or use a different processId.`);
1634
+ }
1635
+ // Step 1: Start the dev server
1636
+ const outputFile = this.createStructuredLogPath(command, undefined, "frontend");
1637
+ this.cleanupOldLogs(3);
1638
+ const [program, ...cmdArgs] = command.split(' ');
1639
+ // Initial log entry
1640
+ writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
1641
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] โœ… Starting development server...\n`);
1642
+ // Spawn the process
1643
+ const devProcess = spawn(program, cmdArgs, {
1644
+ cwd: cwd,
1645
+ env: { ...process.env, ...validatedArgs.env },
1646
+ detached: true,
1647
+ stdio: ['ignore', 'pipe', 'pipe']
1648
+ });
1649
+ const serverInfo = {
1650
+ process: devProcess,
1651
+ command: command,
1652
+ cwd: cwd,
1653
+ outputFile: outputFile,
1654
+ startTime: new Date(),
1655
+ pid: devProcess.pid,
1656
+ processId: processId
1657
+ };
1658
+ this.activeServers.set(processId, serverInfo);
1659
+ // Variables for port detection
1660
+ let detectedPort = validatedArgs.port || null;
1661
+ let serverReady = false;
1662
+ let outputBuffer = "";
1663
+ // Common port patterns
1664
+ const portPatterns = [
1665
+ /(?:Local|Listening|ready).*?(\d{4})/i,
1666
+ /localhost:(\d{4})/,
1667
+ /port\s*:?\s*(\d{4})/i,
1668
+ /on\s+http:\/\/localhost:(\d{4})/i,
1669
+ /Server running at.*?:(\d{4})/i
1670
+ ];
1671
+ // Ready indicators
1672
+ const readyPatterns = [
1673
+ /Local:\s*http:\/\/localhost/i,
1674
+ /ready on http:\/\/localhost/i,
1675
+ /Server running at/i,
1676
+ /Listening on port/i,
1677
+ /started server on/i,
1678
+ /Ready in \d+ms/i,
1679
+ /compiled successfully/i
1680
+ ];
1681
+ // Create promise for server ready detection
1682
+ const serverReadyPromise = new Promise((resolve, reject) => {
1683
+ const timeout = setTimeout(() => {
1684
+ reject(new Error(`Server did not start within ${waitTimeout}ms`));
1685
+ }, waitTimeout);
1686
+ const checkOutput = (data) => {
1687
+ const text = data.toString();
1688
+ outputBuffer += text;
1689
+ // Check for port if not detected
1690
+ if (!detectedPort) {
1691
+ for (const pattern of portPatterns) {
1692
+ const match = outputBuffer.match(pattern);
1693
+ if (match && match[1]) {
1694
+ detectedPort = parseInt(match[1]);
1695
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ๐Ÿ” Detected port: ${detectedPort}\n`);
1696
+ break;
1697
+ }
1698
+ }
1699
+ }
1700
+ // Check for ready state
1701
+ if (!serverReady) {
1702
+ for (const pattern of readyPatterns) {
1703
+ if (pattern.test(outputBuffer)) {
1704
+ serverReady = true;
1705
+ clearTimeout(timeout);
1706
+ // If we have a port, resolve immediately
1707
+ if (detectedPort) {
1708
+ resolve(detectedPort);
1709
+ }
1710
+ else {
1711
+ // Try common ports
1712
+ const commonPorts = [3000, 3001, 5173, 8080, 4200];
1713
+ detectedPort = commonPorts[0]; // Default to 3000
1714
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] โš ๏ธ Could not detect port, defaulting to ${detectedPort}\n`);
1715
+ resolve(detectedPort);
1716
+ }
1717
+ break;
1718
+ }
1719
+ }
1720
+ }
1721
+ };
1722
+ // Attach listeners
1723
+ devProcess.stdout?.on('data', checkOutput);
1724
+ devProcess.stderr?.on('data', checkOutput);
1725
+ });
1726
+ // Stream stdout to file
1727
+ devProcess.stdout?.on('data', (data) => {
1728
+ const timestamp = new Date().toISOString();
1729
+ const logEntry = `[${timestamp}] [${processId}] ${data}`;
1730
+ appendFileSync(outputFile, logEntry);
1731
+ });
1732
+ // Stream stderr to file
1733
+ devProcess.stderr?.on('data', (data) => {
1734
+ const timestamp = new Date().toISOString();
1735
+ const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
1736
+ appendFileSync(outputFile, logEntry);
1737
+ });
1738
+ // Handle process exit
1739
+ devProcess.on('exit', (code, signal) => {
1740
+ const timestamp = new Date().toISOString();
1741
+ const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
1742
+ appendFileSync(outputFile, exitMessage);
1743
+ this.activeServers.delete(processId);
1744
+ this.saveState();
1745
+ });
1746
+ // Handle process errors
1747
+ devProcess.on('error', (error) => {
1748
+ const timestamp = new Date().toISOString();
1749
+ const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
1750
+ appendFileSync(outputFile, errorMessage);
1751
+ });
1752
+ this.saveState();
1753
+ // Step 2: Wait for server to be ready
1754
+ let port;
1755
+ try {
1756
+ port = await serverReadyPromise;
1757
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] โœ… Server ready on http://localhost:${port}\n`);
1758
+ }
1759
+ catch (error) {
1760
+ // Server didn't start properly, clean up
1761
+ const failedServerInfo = this.activeServers.get(processId);
1762
+ if (failedServerInfo) {
1763
+ try {
1764
+ // Close browser if it exists
1765
+ if (failedServerInfo.browser) {
1766
+ await failedServerInfo.browser.close();
1767
+ }
1768
+ // Remove all event listeners to prevent memory leaks
1769
+ if (failedServerInfo.process) {
1770
+ failedServerInfo.process.stdout?.removeAllListeners('data');
1771
+ failedServerInfo.process.stderr?.removeAllListeners('data');
1772
+ failedServerInfo.process.removeAllListeners('exit');
1773
+ failedServerInfo.process.removeAllListeners('error');
1774
+ }
1775
+ // Kill the process
1776
+ if (failedServerInfo.pid) {
1777
+ process.kill(failedServerInfo.pid, 'SIGTERM');
1778
+ }
1779
+ }
1780
+ catch (killError) {
1781
+ // Ignore errors during cleanup
1782
+ }
1783
+ this.activeServers.delete(processId);
1784
+ this.saveState();
1785
+ }
1786
+ throw error;
1787
+ }
1788
+ // Step 3: Wait a bit more for stability
1789
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] โณ Waiting ${browserDelay}ms before launching browser...\n`);
1790
+ await new Promise(resolve => setTimeout(resolve, browserDelay));
1791
+ // Step 4: Launch the test browser
1792
+ try {
1793
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] โœ… Launching test browser...\n`);
1794
+ const browser = await chromium.launch({
1795
+ headless: false,
1796
+ slowMo: teachingMode ? 50 : 0,
1797
+ devtools: teachingMode,
1798
+ args: ['--window-size=1280,800', '--window-position=100,100']
1799
+ });
1800
+ const context = await browser.newContext({
1801
+ viewport: { width: 1280, height: 800 }
1802
+ });
1803
+ const page = await context.newPage();
1804
+ // Add banner to identify test browser
1805
+ await page.addInitScript(() => {
1806
+ // @ts-ignore - browser context
1807
+ const banner = document.createElement('div');
1808
+ banner.id = 'test-browser-banner';
1809
+ banner.innerHTML = '๐Ÿงช TEST BROWSER - All console logs are being captured ๐Ÿ“';
1810
+ banner.style.cssText = `
1811
+ position: fixed;
1812
+ top: 0;
1813
+ left: 0;
1814
+ right: 0;
1815
+ background: #22c55e;
1816
+ color: white;
1817
+ padding: 8px;
1818
+ text-align: center;
1819
+ font-family: monospace;
1820
+ font-size: 14px;
1821
+ z-index: 999999;
1822
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
1823
+ `;
1824
+ // @ts-ignore - browser context
1825
+ if (document.readyState === 'loading') {
1826
+ // @ts-ignore - browser context
1827
+ document.addEventListener('DOMContentLoaded', () => {
1828
+ // @ts-ignore - browser context
1829
+ document.body.insertBefore(banner, document.body.firstChild);
1830
+ });
1831
+ }
1832
+ else {
1833
+ // @ts-ignore - browser context
1834
+ document.body.insertBefore(banner, document.body.firstChild);
1835
+ }
1836
+ });
1837
+ // Set up console capture
1838
+ page.on('console', (msg) => {
1839
+ const timestamp = new Date().toISOString();
1840
+ const msgType = msg.type().toUpperCase();
1841
+ const msgText = msg.text();
1842
+ const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType}] ${msgText}\n`;
1843
+ appendFileSync(outputFile, logEntry);
1844
+ // Flash red on errors
1845
+ if (msgType === 'ERROR') {
1846
+ page.evaluate(() => {
1847
+ // @ts-ignore - browser context
1848
+ const flash = document.createElement('div');
1849
+ flash.style.cssText = `
1850
+ position: fixed;
1851
+ top: 0;
1852
+ left: 0;
1853
+ right: 0;
1854
+ bottom: 0;
1855
+ background: rgba(239, 68, 68, 0.3);
1856
+ z-index: 999998;
1857
+ pointer-events: none;
1858
+ animation: errorFlash 0.5s ease-out;
1859
+ `;
1860
+ // @ts-ignore - browser context
1861
+ const style = document.createElement('style');
1862
+ style.textContent = `
1863
+ @keyframes errorFlash {
1864
+ 0% { opacity: 1; }
1865
+ 100% { opacity: 0; }
1866
+ }
1867
+ `;
1868
+ // @ts-ignore - browser context
1869
+ document.head.appendChild(style);
1870
+ // @ts-ignore - browser context
1871
+ document.body.appendChild(flash);
1872
+ setTimeout(() => flash.remove(), 500);
1873
+ }).catch(() => { }); // Ignore errors from flashing
1874
+ }
1875
+ });
1876
+ // Navigate to the app
1877
+ const browserUrl = `http://localhost:${port}`;
1878
+ await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
1879
+ // Update server info with browser details
1880
+ serverInfo.browser = browser;
1881
+ serverInfo.page = page;
1882
+ serverInfo.browserUrl = browserUrl;
1883
+ serverInfo.consoleCapture = true;
1884
+ // Log success
1885
+ const successMessage = `
1886
+ ๐ŸŽฏ TEST BROWSER LAUNCHED!
1887
+
1888
+ ๐Ÿ‘‰ Use the browser window that just opened (with green banner)
1889
+ ๐Ÿ“ All console logs are being saved
1890
+ ๐Ÿ” DevTools is open for debugging
1891
+ โš ๏ธ Errors will flash red on screen
1892
+
1893
+ Happy debugging! ๐Ÿš€
1894
+ `;
1895
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`);
1896
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ${successMessage}\n`);
1897
+ return {
1898
+ content: [
1899
+ {
1900
+ type: "text",
1901
+ text: JSON.stringify({
1902
+ status: "success",
1903
+ message: successMessage.trim(),
1904
+ processId: processId,
1905
+ serverPid: devProcess.pid,
1906
+ serverUrl: `http://localhost:${port}`,
1907
+ logFile: outputFile,
1908
+ browserStatus: "launched",
1909
+ teachingMode: teachingMode
1910
+ }, null, 2)
1911
+ }
1912
+ ]
1913
+ };
1914
+ }
1915
+ catch (browserError) {
1916
+ // Browser launch failed, but server is running
1917
+ const errorMsg = browserError instanceof Error ? browserError.message : String(browserError);
1918
+ appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [ERROR] Failed to launch browser: ${errorMsg}\n`);
1919
+ return {
1920
+ content: [
1921
+ {
1922
+ type: "text",
1923
+ text: JSON.stringify({
1924
+ status: "partial_success",
1925
+ message: `Server started successfully on http://localhost:${port}, but browser launch failed`,
1926
+ error: errorMsg,
1927
+ processId: processId,
1928
+ serverPid: devProcess.pid,
1929
+ serverUrl: `http://localhost:${port}`,
1930
+ logFile: outputFile,
1931
+ browserStatus: "failed",
1932
+ manualInstructions: "Open http://localhost:" + port + " in your browser manually"
1933
+ }, null, 2)
1934
+ }
1935
+ ]
1936
+ };
1937
+ }
1938
+ }
1939
+ catch (error) {
1940
+ throw new Error(`Failed to start frontend with browser: ${error instanceof Error ? error.message : String(error)}`);
1941
+ }
1942
+ }
1190
1943
  default:
1191
1944
  throw new Error(`Unknown tool: ${name}`);
1192
1945
  }