@chinchillaenterprises/mcp-dev-logger 2.3.3 → 3.0.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/README.md +127 -495
- package/dist/index.js +272 -2087
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -4,2131 +4,316 @@ 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,
|
|
8
|
-
import { join
|
|
9
|
-
import { tmpdir } from "os";
|
|
7
|
+
import { writeFileSync, existsSync, appendFileSync, mkdirSync, readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
10
9
|
import { promisify } from "util";
|
|
11
|
-
import { chromium } from "playwright";
|
|
12
10
|
const execAsync = promisify(exec);
|
|
13
|
-
//
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
env: z.record(z.string()).optional().describe("Environment variables"),
|
|
19
|
-
processId: z.string().optional().describe("Custom process ID (auto-generated if not provided)"),
|
|
20
|
-
structuredLogging: z.boolean().optional().describe("Use organized logging with date-based sessions (default: true)"),
|
|
21
|
-
sessionDate: z.string().optional().describe("Session date for organized logging (YYYY-MM-DD, defaults to today)"),
|
|
22
|
-
logType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Process type for standardized naming (auto-detected if not provided)")
|
|
11
|
+
// Simple schema - no parameters needed
|
|
12
|
+
const StartSandboxArgsSchema = z.object({});
|
|
13
|
+
const StopSandboxArgsSchema = z.object({});
|
|
14
|
+
const TailSandboxLogsArgsSchema = z.object({
|
|
15
|
+
lines: z.number().optional().describe("Number of lines to return (default: 50)")
|
|
23
16
|
});
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
lines: z.number().optional().describe("Number of lines to return (default: 50)"),
|
|
34
|
-
filter: z.string().optional().describe("Grep pattern to filter logs")
|
|
35
|
-
});
|
|
36
|
-
const ClearLogsArgsSchema = z.object({
|
|
37
|
-
processId: z.string().optional().describe("Process ID to clear logs for (if not provided, tries to find single process)"),
|
|
38
|
-
backup: z.boolean().optional().describe("Backup logs before clearing (default: false)")
|
|
39
|
-
});
|
|
40
|
-
const GetProcessesArgsSchema = z.object({
|
|
41
|
-
processId: z.string().optional().describe("Process ID to get info for (if not provided, shows all processes)"),
|
|
42
|
-
format: z.enum(["raw", "formatted"]).optional().describe("Output format: raw (milliseconds) or formatted (hours/minutes/seconds) - default: formatted")
|
|
43
|
-
});
|
|
44
|
-
const CheckRunningProcessesArgsSchema = z.object({
|
|
45
|
-
processType: z.enum(["frontend", "backend", "amplify", "custom"]).optional().describe("Type of process to check for conflicts"),
|
|
46
|
-
port: z.number().optional().describe("Specific port to check")
|
|
47
|
-
});
|
|
48
|
-
const DiscoverLogsArgsSchema = z.object({
|
|
49
|
-
sessionDate: z.string().optional().describe("Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)")
|
|
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
|
-
});
|
|
61
|
-
class DevLoggerServer {
|
|
62
|
-
server;
|
|
63
|
-
activeServers;
|
|
64
|
-
pidFile;
|
|
65
|
-
constructor() {
|
|
66
|
-
this.server = new Server({
|
|
67
|
-
name: "mcp-dev-logger",
|
|
68
|
-
version: "1.0.0",
|
|
69
|
-
}, {
|
|
70
|
-
capabilities: {
|
|
71
|
-
tools: {},
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
this.activeServers = new Map();
|
|
75
|
-
this.pidFile = join(tmpdir(), "mcp-dev-logger.json");
|
|
76
|
-
// Load any saved state
|
|
77
|
-
this.loadState();
|
|
78
|
-
this.setupHandlers();
|
|
17
|
+
const HARDCODED_COMMAND = "npx ampx sandbox --stream-function-logs";
|
|
18
|
+
let sandboxProcess = null;
|
|
19
|
+
// Get log file path - always in resources/sandbox/sandbox.log relative to cwd
|
|
20
|
+
function getLogFilePath() {
|
|
21
|
+
const cwd = process.cwd();
|
|
22
|
+
const sandboxDir = join(cwd, "resources", "sandbox");
|
|
23
|
+
// Create resources/sandbox directory if it doesn't exist
|
|
24
|
+
if (!existsSync(sandboxDir)) {
|
|
25
|
+
mkdirSync(sandboxDir, { recursive: true });
|
|
79
26
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
27
|
+
return join(sandboxDir, "sandbox.log");
|
|
28
|
+
}
|
|
29
|
+
// Find existing sandbox process for current directory
|
|
30
|
+
async function findExistingSandbox() {
|
|
31
|
+
const currentDir = process.cwd();
|
|
32
|
+
try {
|
|
33
|
+
// Get all ampx sandbox processes
|
|
34
|
+
const { stdout } = await execAsync("ps aux | grep 'ampx sandbox' | grep -v grep");
|
|
35
|
+
if (!stdout.trim()) {
|
|
36
|
+
return null; // No sandboxes running
|
|
37
|
+
}
|
|
38
|
+
// Extract PIDs from ps output
|
|
39
|
+
const lines = stdout.trim().split('\n');
|
|
40
|
+
const pids = [];
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const match = line.match(/^\S+\s+(\d+)/);
|
|
43
|
+
if (match) {
|
|
44
|
+
pids.push(parseInt(match[1]));
|
|
86
45
|
}
|
|
87
46
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
47
|
+
// Check each PID's working directory
|
|
48
|
+
for (const pid of pids) {
|
|
49
|
+
try {
|
|
50
|
+
const { stdout: lsofOutput } = await execAsync(`lsof -a -d cwd -p ${pid} 2>/dev/null | tail -1`);
|
|
51
|
+
// Extract path from lsof output (fields 9 onwards)
|
|
52
|
+
const fields = lsofOutput.trim().split(/\s+/);
|
|
53
|
+
const pathFields = fields.slice(8); // Starting from index 8 (field 9 in 1-indexed)
|
|
54
|
+
const workingDir = pathFields.join(' ').trim();
|
|
55
|
+
if (workingDir === currentDir) {
|
|
56
|
+
return pid; // Found our sandbox!
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Process might have exited, continue checking others
|
|
61
|
+
continue;
|
|
104
62
|
}
|
|
105
|
-
writeFileSync(this.pidFile, JSON.stringify(state, null, 2));
|
|
106
|
-
}
|
|
107
|
-
catch (error) {
|
|
108
|
-
// Ignore errors saving state
|
|
109
63
|
}
|
|
64
|
+
return null; // No sandbox found for this directory
|
|
110
65
|
}
|
|
111
|
-
|
|
112
|
-
//
|
|
113
|
-
const commandPart = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
|
|
114
|
-
const filePart = outputFile.split('/').pop()?.replace(/[^a-zA-Z0-9]/g, '').replace(/\.(txt|log)$/, '') || 'default';
|
|
115
|
-
return `${commandPart}-${filePart}`;
|
|
116
|
-
}
|
|
117
|
-
findProcessOrDefault(processId) {
|
|
118
|
-
if (processId) {
|
|
119
|
-
return this.activeServers.has(processId) ? processId : null;
|
|
120
|
-
}
|
|
121
|
-
// If no processId provided and only one process running, use that
|
|
122
|
-
const activeIds = Array.from(this.activeServers.keys());
|
|
123
|
-
if (activeIds.length === 1) {
|
|
124
|
-
return activeIds[0];
|
|
125
|
-
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
// No processes found or error occurred
|
|
126
68
|
return null;
|
|
127
69
|
}
|
|
128
|
-
|
|
70
|
+
}
|
|
71
|
+
// Start Amplify sandbox with streaming logs
|
|
72
|
+
async function startSandbox() {
|
|
73
|
+
// Check for existing sandbox process in this directory
|
|
74
|
+
const existingPid = await findExistingSandbox();
|
|
75
|
+
const logFile = getLogFilePath();
|
|
76
|
+
let killedPid;
|
|
77
|
+
if (existingPid) {
|
|
78
|
+
// Found existing sandbox, kill it first
|
|
129
79
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const { stdout: cmdStdout } = await execAsync(`ps -p ${pid} -o command=`);
|
|
137
|
-
return {
|
|
138
|
-
inUse: true,
|
|
139
|
-
pid: pid,
|
|
140
|
-
command: cmdStdout.trim()
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
return { inUse: true, pid: pid };
|
|
145
|
-
}
|
|
146
|
-
}
|
|
80
|
+
process.kill(existingPid, 'SIGTERM');
|
|
81
|
+
killedPid = existingPid;
|
|
82
|
+
const timestamp = new Date().toISOString();
|
|
83
|
+
appendFileSync(logFile, `[${timestamp}] Killed existing sandbox (PID: ${existingPid}), starting fresh...\n`, "utf-8");
|
|
84
|
+
// Wait a bit for cleanup
|
|
85
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
147
86
|
}
|
|
148
|
-
catch (
|
|
149
|
-
//
|
|
87
|
+
catch (err) {
|
|
88
|
+
// Process might already be dead, continue anyway
|
|
150
89
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const cmd = command.toLowerCase();
|
|
155
|
-
if (cmd.includes('next dev') || cmd.includes('npm run dev') || cmd.includes('yarn dev') || cmd.includes('pnpm dev') || cmd.includes('vite')) {
|
|
156
|
-
return 'frontend';
|
|
157
|
-
}
|
|
158
|
-
if (cmd.includes('ampx sandbox') || cmd.includes('amplify sandbox')) {
|
|
159
|
-
return 'amplify';
|
|
160
|
-
}
|
|
161
|
-
if (cmd.includes('node server') || cmd.includes('express') || cmd.includes('fastify') || cmd.includes('api')) {
|
|
162
|
-
return 'backend';
|
|
163
|
-
}
|
|
164
|
-
return 'custom';
|
|
165
|
-
}
|
|
166
|
-
getCommonPorts(processType) {
|
|
167
|
-
switch (processType) {
|
|
168
|
-
case 'frontend':
|
|
169
|
-
return [3000, 3001, 5173, 5174, 8080, 8081];
|
|
170
|
-
case 'backend':
|
|
171
|
-
return [3001, 8000, 8080, 4000, 5000];
|
|
172
|
-
case 'amplify':
|
|
173
|
-
return []; // Amplify doesn't typically use fixed ports
|
|
174
|
-
default:
|
|
175
|
-
return [3000, 3001, 5173, 5174, 8000, 8080, 8081, 4000, 5000];
|
|
90
|
+
// Clear our tracked process if it was the one we killed
|
|
91
|
+
if (sandboxProcess?.pid === existingPid) {
|
|
92
|
+
sandboxProcess = null;
|
|
176
93
|
}
|
|
177
94
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
95
|
+
// Start new sandbox
|
|
96
|
+
// Clear/overwrite the log file
|
|
97
|
+
writeFileSync(logFile, "", "utf-8");
|
|
98
|
+
// Write initial log entry
|
|
99
|
+
const timestamp = new Date().toISOString();
|
|
100
|
+
appendFileSync(logFile, `[${timestamp}] Starting Amplify sandbox: ${HARDCODED_COMMAND}\n`, "utf-8");
|
|
101
|
+
// Start the process
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
const proc = spawn("npx", ["ampx", "sandbox", "--stream-function-logs"], {
|
|
104
|
+
cwd,
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
106
|
+
detached: false,
|
|
107
|
+
shell: false
|
|
108
|
+
});
|
|
109
|
+
sandboxProcess = {
|
|
110
|
+
process: proc,
|
|
111
|
+
pid: proc.pid,
|
|
112
|
+
startTime: new Date()
|
|
113
|
+
};
|
|
114
|
+
// Stream stdout to log file
|
|
115
|
+
proc.stdout?.on("data", (data) => {
|
|
116
|
+
const timestamp = new Date().toISOString();
|
|
117
|
+
const lines = data.toString().split("\n");
|
|
118
|
+
lines.forEach(line => {
|
|
119
|
+
if (line.trim()) {
|
|
120
|
+
appendFileSync(logFile, `[${timestamp}] ${line}\n`, "utf-8");
|
|
195
121
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// Stream stderr to log file
|
|
125
|
+
proc.stderr?.on("data", (data) => {
|
|
126
|
+
const timestamp = new Date().toISOString();
|
|
127
|
+
const lines = data.toString().split("\n");
|
|
128
|
+
lines.forEach(line => {
|
|
129
|
+
if (line.trim()) {
|
|
130
|
+
appendFileSync(logFile, `[${timestamp}] [ERROR] ${line}\n`, "utf-8");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// Handle process exit
|
|
135
|
+
proc.on("exit", (code, signal) => {
|
|
136
|
+
const timestamp = new Date().toISOString();
|
|
137
|
+
appendFileSync(logFile, `[${timestamp}] Amplify sandbox exited with code ${code} and signal ${signal}\n`, "utf-8");
|
|
138
|
+
sandboxProcess = null;
|
|
139
|
+
});
|
|
140
|
+
proc.on("error", (err) => {
|
|
141
|
+
const timestamp = new Date().toISOString();
|
|
142
|
+
appendFileSync(logFile, `[${timestamp}] [ERROR] Failed to start Amplify sandbox: ${err.message}\n`, "utf-8");
|
|
143
|
+
sandboxProcess = null;
|
|
144
|
+
});
|
|
145
|
+
const message = killedPid
|
|
146
|
+
? `Killed existing sandbox (PID: ${killedPid}) and started fresh. Logs are being written to ${logFile}`
|
|
147
|
+
: `Amplify sandbox started. Logs are being written to ${logFile}`;
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
message,
|
|
151
|
+
logFile,
|
|
152
|
+
killedPid
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Stop Amplify sandbox
|
|
156
|
+
function stopSandbox() {
|
|
157
|
+
if (!sandboxProcess?.process) {
|
|
209
158
|
return {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
availablePorts: availablePorts.slice(0, 5), // Limit to first 5 available
|
|
213
|
-
recommendations
|
|
159
|
+
success: false,
|
|
160
|
+
message: "No Amplify sandbox process is running."
|
|
214
161
|
};
|
|
215
162
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
return 'custom';
|
|
228
|
-
}
|
|
229
|
-
getLogFilesFromDirectory(dirPath) {
|
|
230
|
-
if (!existsSync(dirPath)) {
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
const logFiles = [];
|
|
234
|
-
try {
|
|
235
|
-
const files = readdirSync(dirPath);
|
|
236
|
-
for (const file of files) {
|
|
237
|
-
// Skip non-log files and hidden files
|
|
238
|
-
if (!file.endsWith('.log') && !file.endsWith('.txt') || file.startsWith('.')) {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
const filePath = join(dirPath, file);
|
|
242
|
-
const stats = statSync(filePath);
|
|
243
|
-
// Get line count for log files
|
|
244
|
-
let lineCount;
|
|
245
|
-
try {
|
|
246
|
-
const content = readFileSync(filePath, 'utf8');
|
|
247
|
-
lineCount = content.split('\n').filter(line => line.trim()).length;
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
// Ignore errors reading file content
|
|
251
|
-
}
|
|
252
|
-
logFiles.push({
|
|
253
|
-
fileName: file,
|
|
254
|
-
filePath: filePath,
|
|
255
|
-
processType: this.detectLogProcessType(file),
|
|
256
|
-
size: stats.size,
|
|
257
|
-
lastModified: stats.mtime,
|
|
258
|
-
lineCount
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
// Directory read error - return empty array
|
|
264
|
-
}
|
|
265
|
-
return logFiles.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
163
|
+
const logFile = getLogFilePath();
|
|
164
|
+
const timestamp = new Date().toISOString();
|
|
165
|
+
appendFileSync(logFile, `[${timestamp}] Stopping Amplify sandbox...\n`, "utf-8");
|
|
166
|
+
try {
|
|
167
|
+
sandboxProcess.process.kill("SIGTERM");
|
|
168
|
+
sandboxProcess = null;
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
message: "Amplify sandbox stopped successfully."
|
|
172
|
+
};
|
|
266
173
|
}
|
|
267
|
-
|
|
268
|
-
const cwd = process.cwd();
|
|
269
|
-
const logsDir = join(cwd, 'logs');
|
|
270
|
-
// If logs directory doesn't exist, check current directory for log files
|
|
271
|
-
if (!existsSync(logsDir)) {
|
|
272
|
-
const currentDirLogs = this.getLogFilesFromDirectory(cwd);
|
|
273
|
-
if (currentDirLogs.length === 0) {
|
|
274
|
-
return {
|
|
275
|
-
status: 'no_logs_found',
|
|
276
|
-
availableSessions: [],
|
|
277
|
-
recommendedLogs: ['No log files found. Start a dev server to create logs.'],
|
|
278
|
-
totalSessions: 0
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
// Return current directory logs as a session
|
|
282
|
-
return {
|
|
283
|
-
status: 'success',
|
|
284
|
-
mostRecentSession: {
|
|
285
|
-
sessionDate: new Date().toISOString().split('T')[0],
|
|
286
|
-
logFiles: currentDirLogs,
|
|
287
|
-
totalFiles: currentDirLogs.length,
|
|
288
|
-
sessionPath: cwd
|
|
289
|
-
},
|
|
290
|
-
availableSessions: ['current'],
|
|
291
|
-
recommendedLogs: currentDirLogs.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines)`),
|
|
292
|
-
totalSessions: 1
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
// Discover date-based sessions in logs directory
|
|
296
|
-
const sessions = [];
|
|
297
|
-
let mostRecentSession;
|
|
298
|
-
try {
|
|
299
|
-
const entries = readdirSync(logsDir);
|
|
300
|
-
for (const entry of entries) {
|
|
301
|
-
const entryPath = join(logsDir, entry);
|
|
302
|
-
const stats = statSync(entryPath);
|
|
303
|
-
if (stats.isDirectory() && /^\d{4}-\d{2}-\d{2}$/.test(entry)) {
|
|
304
|
-
sessions.push(entry);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Sort sessions by date (most recent first)
|
|
308
|
-
sessions.sort((a, b) => b.localeCompare(a));
|
|
309
|
-
// Get the target session (specified or most recent)
|
|
310
|
-
const targetSession = sessionDate || sessions[0];
|
|
311
|
-
if (targetSession && sessions.includes(targetSession)) {
|
|
312
|
-
const sessionPath = join(logsDir, targetSession);
|
|
313
|
-
const logFiles = this.getLogFilesFromDirectory(sessionPath);
|
|
314
|
-
mostRecentSession = {
|
|
315
|
-
sessionDate: targetSession,
|
|
316
|
-
logFiles,
|
|
317
|
-
totalFiles: logFiles.length,
|
|
318
|
-
sessionPath
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
catch (error) {
|
|
323
|
-
// Directory read error
|
|
324
|
-
}
|
|
325
|
-
const recommendedLogs = [];
|
|
326
|
-
if (mostRecentSession) {
|
|
327
|
-
if (mostRecentSession.logFiles.length === 0) {
|
|
328
|
-
recommendedLogs.push(`Session ${mostRecentSession.sessionDate} has no log files`);
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
recommendedLogs.push(`Most recent session: ${mostRecentSession.sessionDate}`);
|
|
332
|
-
recommendedLogs.push(...mostRecentSession.logFiles.map(f => `${f.processType}: ${f.fileName} (${f.lineCount || 0} lines, ${Math.round(f.size / 1024)}KB)`));
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
else if (sessions.length === 0) {
|
|
336
|
-
recommendedLogs.push('No organized log sessions found. Logs will be created in logs/YYYY-MM-DD/ when you start dev servers.');
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
recommendedLogs.push('No logs found in recent sessions. Start dev servers to generate logs.');
|
|
340
|
-
}
|
|
174
|
+
catch (err) {
|
|
341
175
|
return {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
availableSessions: sessions,
|
|
345
|
-
recommendedLogs,
|
|
346
|
-
totalSessions: sessions.length
|
|
176
|
+
success: false,
|
|
177
|
+
message: `Failed to stop Amplify sandbox: ${err instanceof Error ? err.message : String(err)}`
|
|
347
178
|
};
|
|
348
179
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
if (!existsSync(sessionDir)) {
|
|
360
|
-
mkdirSync(sessionDir, { recursive: true });
|
|
361
|
-
}
|
|
362
|
-
// Determine log type and create process type subfolder
|
|
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
|
|
369
|
-
const standardizedName = this.generateStandardizedLogName(detectedType, command);
|
|
370
|
-
return join(processTypeDir, standardizedName);
|
|
180
|
+
}
|
|
181
|
+
// Tail sandbox logs
|
|
182
|
+
function tailSandboxLogs(lines = 50) {
|
|
183
|
+
const logFile = getLogFilePath();
|
|
184
|
+
if (!existsSync(logFile)) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
message: "No log file found. Start the sandbox first with dev_start_sandbox."
|
|
188
|
+
};
|
|
371
189
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
return `vite-${timestamp}.log`;
|
|
381
|
-
if (command.includes('react'))
|
|
382
|
-
return `react-${timestamp}.log`;
|
|
383
|
-
return `dev-${timestamp}.log`;
|
|
384
|
-
case 'backend':
|
|
385
|
-
if (command.includes('express'))
|
|
386
|
-
return `express-${timestamp}.log`;
|
|
387
|
-
if (command.includes('fastify'))
|
|
388
|
-
return `fastify-${timestamp}.log`;
|
|
389
|
-
if (command.includes('node'))
|
|
390
|
-
return `node-${timestamp}.log`;
|
|
391
|
-
return `api-${timestamp}.log`;
|
|
392
|
-
case 'amplify':
|
|
393
|
-
if (command.includes('sandbox'))
|
|
394
|
-
return `sandbox-${timestamp}.log`;
|
|
395
|
-
if (command.includes('deploy'))
|
|
396
|
-
return `deploy-${timestamp}.log`;
|
|
397
|
-
return `dev-${timestamp}.log`;
|
|
398
|
-
default:
|
|
399
|
-
// Extract first word of command for custom processes
|
|
400
|
-
const cmdName = command.split(' ')[0].replace(/[^a-zA-Z0-9]/g, '');
|
|
401
|
-
return `${cmdName}-${timestamp}.log`;
|
|
402
|
-
}
|
|
190
|
+
try {
|
|
191
|
+
const content = readFileSync(logFile, "utf-8");
|
|
192
|
+
const allLines = content.split("\n").filter(line => line.trim());
|
|
193
|
+
const tailLines = allLines.slice(-lines);
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
logs: tailLines.join("\n")
|
|
197
|
+
};
|
|
403
198
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (args.outputFile && args.structuredLogging !== true)
|
|
410
|
-
return false;
|
|
411
|
-
return true;
|
|
199
|
+
catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
message: `Failed to read logs: ${err instanceof Error ? err.message : String(err)}`
|
|
203
|
+
};
|
|
412
204
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
205
|
+
}
|
|
206
|
+
// Initialize MCP server
|
|
207
|
+
const server = new Server({
|
|
208
|
+
name: "mcp-dev-logger",
|
|
209
|
+
version: "3.0.0",
|
|
210
|
+
}, {
|
|
211
|
+
capabilities: {
|
|
212
|
+
tools: {},
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
// List available tools
|
|
216
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
217
|
+
const tools = [
|
|
218
|
+
{
|
|
219
|
+
name: "dev_start_sandbox",
|
|
220
|
+
description: "Start Amplify Gen2 sandbox with streaming function logs. Runs 'npx ampx sandbox --stream-function-logs' and logs to resources/sandbox.log (overwrites previous logs).",
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {},
|
|
224
|
+
required: []
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "dev_stop_sandbox",
|
|
229
|
+
description: "Stop the running Amplify sandbox process.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {},
|
|
233
|
+
required: []
|
|
420
234
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "dev_tail_sandbox_logs",
|
|
238
|
+
description: "Get the last N lines from the sandbox log file.",
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
lines: {
|
|
243
|
+
type: "number",
|
|
244
|
+
description: "Number of lines to return (default: 50)"
|
|
443
245
|
}
|
|
444
|
-
}
|
|
246
|
+
},
|
|
247
|
+
required: []
|
|
445
248
|
}
|
|
446
249
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// Remove all event listeners to prevent memory leaks
|
|
461
|
-
info.process.stdout?.removeAllListeners('data');
|
|
462
|
-
info.process.stderr?.removeAllListeners('data');
|
|
463
|
-
info.process.removeAllListeners('exit');
|
|
464
|
-
info.process.removeAllListeners('error');
|
|
465
|
-
// Kill the process
|
|
466
|
-
if (info.pid) {
|
|
467
|
-
// Kill the process group (negative PID) to kill all children
|
|
468
|
-
process.kill(-info.pid, 'SIGTERM');
|
|
469
|
-
}
|
|
470
|
-
}
|
|
250
|
+
];
|
|
251
|
+
return { tools };
|
|
252
|
+
});
|
|
253
|
+
// Handle tool calls
|
|
254
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
255
|
+
const { name, arguments: args } = request.params;
|
|
256
|
+
try {
|
|
257
|
+
switch (name) {
|
|
258
|
+
case "dev_start_sandbox": {
|
|
259
|
+
const result = await startSandbox();
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
262
|
+
};
|
|
471
263
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
catch (fallbackError) {
|
|
480
|
-
// Ignore - process is definitely dead
|
|
481
|
-
}
|
|
264
|
+
case "dev_stop_sandbox": {
|
|
265
|
+
const result = stopSandbox();
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
268
|
+
};
|
|
482
269
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
{
|
|
490
|
-
name: "dev_start_log_streaming",
|
|
491
|
-
description: "Start development server with organized logging (logs/YYYY-MM-DD/). Supports structured sessions and standardized file naming.",
|
|
492
|
-
inputSchema: {
|
|
493
|
-
type: "object",
|
|
494
|
-
properties: {
|
|
495
|
-
command: {
|
|
496
|
-
type: "string",
|
|
497
|
-
description: "Dev command to run (default: npm run dev)"
|
|
498
|
-
},
|
|
499
|
-
outputFile: {
|
|
500
|
-
type: "string",
|
|
501
|
-
description: "File to write logs to (DEPRECATED: use structuredLogging instead)"
|
|
502
|
-
},
|
|
503
|
-
cwd: {
|
|
504
|
-
type: "string",
|
|
505
|
-
description: "Working directory"
|
|
506
|
-
},
|
|
507
|
-
env: {
|
|
508
|
-
type: "object",
|
|
509
|
-
description: "Environment variables",
|
|
510
|
-
additionalProperties: {
|
|
511
|
-
type: "string"
|
|
512
|
-
}
|
|
513
|
-
},
|
|
514
|
-
processId: {
|
|
515
|
-
type: "string",
|
|
516
|
-
description: "Custom process ID (auto-generated if not provided)"
|
|
517
|
-
},
|
|
518
|
-
structuredLogging: {
|
|
519
|
-
type: "boolean",
|
|
520
|
-
description: "Use organized logging with date-based sessions (default: true)"
|
|
521
|
-
},
|
|
522
|
-
sessionDate: {
|
|
523
|
-
type: "string",
|
|
524
|
-
description: "Session date for organized logging (YYYY-MM-DD, defaults to today)"
|
|
525
|
-
},
|
|
526
|
-
logType: {
|
|
527
|
-
type: "string",
|
|
528
|
-
enum: ["frontend", "backend", "amplify", "custom"],
|
|
529
|
-
description: "Process type for standardized naming (auto-detected if not provided)"
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
name: "dev_get_processes",
|
|
536
|
-
description: "Get information about running development processes with optional formatting",
|
|
537
|
-
inputSchema: {
|
|
538
|
-
type: "object",
|
|
539
|
-
properties: {
|
|
540
|
-
processId: {
|
|
541
|
-
type: "string",
|
|
542
|
-
description: "Process ID to get info for (if not provided, shows all processes)"
|
|
543
|
-
},
|
|
544
|
-
format: {
|
|
545
|
-
type: "string",
|
|
546
|
-
enum: ["raw", "formatted"],
|
|
547
|
-
description: "Output format: raw (milliseconds) or formatted (hours/minutes/seconds) - default: formatted"
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
name: "dev_stop_log_streaming",
|
|
554
|
-
description: "Stop development server(s) and logging",
|
|
555
|
-
inputSchema: {
|
|
556
|
-
type: "object",
|
|
557
|
-
properties: {
|
|
558
|
-
processId: {
|
|
559
|
-
type: "string",
|
|
560
|
-
description: "Process ID to stop (if not provided, stops all processes)"
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: "dev_restart_log_streaming",
|
|
567
|
-
description: "Restart a specific development server",
|
|
568
|
-
inputSchema: {
|
|
569
|
-
type: "object",
|
|
570
|
-
properties: {
|
|
571
|
-
processId: {
|
|
572
|
-
type: "string",
|
|
573
|
-
description: "Process ID to restart"
|
|
574
|
-
},
|
|
575
|
-
clearLogs: {
|
|
576
|
-
type: "boolean",
|
|
577
|
-
description: "Clear logs on restart (default: false)"
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
required: ["processId"]
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
name: "dev_tail_logs",
|
|
585
|
-
description: "Get last N lines from log file",
|
|
586
|
-
inputSchema: {
|
|
587
|
-
type: "object",
|
|
588
|
-
properties: {
|
|
589
|
-
processId: {
|
|
590
|
-
type: "string",
|
|
591
|
-
description: "Process ID to tail logs from (if not provided, tries to find single process)"
|
|
592
|
-
},
|
|
593
|
-
lines: {
|
|
594
|
-
type: "number",
|
|
595
|
-
description: "Number of lines to return (default: 50)"
|
|
596
|
-
},
|
|
597
|
-
filter: {
|
|
598
|
-
type: "string",
|
|
599
|
-
description: "Grep pattern to filter logs"
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
},
|
|
604
|
-
{
|
|
605
|
-
name: "dev_clear_logs",
|
|
606
|
-
description: "Clear the log file",
|
|
607
|
-
inputSchema: {
|
|
608
|
-
type: "object",
|
|
609
|
-
properties: {
|
|
610
|
-
processId: {
|
|
611
|
-
type: "string",
|
|
612
|
-
description: "Process ID to clear logs for (if not provided, tries to find single process)"
|
|
613
|
-
},
|
|
614
|
-
backup: {
|
|
615
|
-
type: "boolean",
|
|
616
|
-
description: "Backup logs before clearing (default: false)"
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
},
|
|
621
|
-
{
|
|
622
|
-
name: "dev_check_running_processes",
|
|
623
|
-
description: "Check for running development processes that might conflict with new servers",
|
|
624
|
-
inputSchema: {
|
|
625
|
-
type: "object",
|
|
626
|
-
properties: {
|
|
627
|
-
processType: {
|
|
628
|
-
type: "string",
|
|
629
|
-
enum: ["frontend", "backend", "amplify", "custom"],
|
|
630
|
-
description: "Type of process to check for conflicts"
|
|
631
|
-
},
|
|
632
|
-
port: {
|
|
633
|
-
type: "number",
|
|
634
|
-
description: "Specific port to check"
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
name: "dev_discover_logs",
|
|
641
|
-
description: "Auto-discover available log files and sessions in organized directory structure",
|
|
642
|
-
inputSchema: {
|
|
643
|
-
type: "object",
|
|
644
|
-
properties: {
|
|
645
|
-
sessionDate: {
|
|
646
|
-
type: "string",
|
|
647
|
-
description: "Specific session date (YYYY-MM-DD) to discover logs for (defaults to most recent)"
|
|
648
|
-
}
|
|
649
|
-
}
|
|
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: 1920 },
|
|
674
|
-
height: { type: "number", default: 1080 }
|
|
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
|
-
}
|
|
738
|
-
}
|
|
739
|
-
];
|
|
740
|
-
return { tools };
|
|
741
|
-
});
|
|
742
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
743
|
-
try {
|
|
744
|
-
const { name, arguments: args } = request.params;
|
|
745
|
-
switch (name) {
|
|
746
|
-
case "dev_start_log_streaming": {
|
|
747
|
-
const validatedArgs = StartLogStreamingArgsSchema.parse(args);
|
|
748
|
-
const command = validatedArgs.command || "npm run dev";
|
|
749
|
-
const cwd = resolve(validatedArgs.cwd || process.cwd());
|
|
750
|
-
// Determine output file path using structured logging or legacy approach
|
|
751
|
-
let outputFile;
|
|
752
|
-
if (this.shouldUseStructuredLogging(validatedArgs)) {
|
|
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);
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
outputFile = resolve(validatedArgs.outputFile || "dev-server-logs.txt");
|
|
759
|
-
}
|
|
760
|
-
// Generate or use provided process ID
|
|
761
|
-
let processId = validatedArgs.processId || this.generateProcessId(command, outputFile);
|
|
762
|
-
// Ensure unique process ID
|
|
763
|
-
let counter = 1;
|
|
764
|
-
const originalProcessId = processId;
|
|
765
|
-
while (this.activeServers.has(processId)) {
|
|
766
|
-
processId = `${originalProcessId}-${counter}`;
|
|
767
|
-
counter++;
|
|
768
|
-
}
|
|
769
|
-
// Parse command into program and args
|
|
770
|
-
const [program, ...cmdArgs] = command.split(' ');
|
|
771
|
-
// Create or clear the output file
|
|
772
|
-
writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
|
|
773
|
-
try {
|
|
774
|
-
// Spawn the process (detached to avoid signal propagation)
|
|
775
|
-
const devProcess = spawn(program, cmdArgs, {
|
|
776
|
-
cwd: cwd,
|
|
777
|
-
env: { ...process.env, ...validatedArgs.env },
|
|
778
|
-
detached: true,
|
|
779
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
780
|
-
});
|
|
781
|
-
const serverInfo = {
|
|
782
|
-
process: devProcess,
|
|
783
|
-
command: command,
|
|
784
|
-
cwd: cwd,
|
|
785
|
-
outputFile: outputFile,
|
|
786
|
-
startTime: new Date(),
|
|
787
|
-
pid: devProcess.pid,
|
|
788
|
-
processId: processId
|
|
789
|
-
};
|
|
790
|
-
this.activeServers.set(processId, serverInfo);
|
|
791
|
-
// Stream stdout to file
|
|
792
|
-
devProcess.stdout?.on('data', (data) => {
|
|
793
|
-
const timestamp = new Date().toISOString();
|
|
794
|
-
const logEntry = `[${timestamp}] [${processId}] ${data}`;
|
|
795
|
-
appendFileSync(outputFile, logEntry);
|
|
796
|
-
});
|
|
797
|
-
// Stream stderr to file
|
|
798
|
-
devProcess.stderr?.on('data', (data) => {
|
|
799
|
-
const timestamp = new Date().toISOString();
|
|
800
|
-
const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
|
|
801
|
-
appendFileSync(outputFile, logEntry);
|
|
802
|
-
});
|
|
803
|
-
// Handle process exit
|
|
804
|
-
devProcess.on('exit', (code, signal) => {
|
|
805
|
-
const timestamp = new Date().toISOString();
|
|
806
|
-
const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
|
|
807
|
-
appendFileSync(outputFile, exitMessage);
|
|
808
|
-
this.activeServers.delete(processId);
|
|
809
|
-
this.saveState();
|
|
810
|
-
});
|
|
811
|
-
// Handle process errors
|
|
812
|
-
devProcess.on('error', (error) => {
|
|
813
|
-
const timestamp = new Date().toISOString();
|
|
814
|
-
const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
|
|
815
|
-
appendFileSync(outputFile, errorMessage);
|
|
816
|
-
});
|
|
817
|
-
this.saveState();
|
|
818
|
-
return {
|
|
819
|
-
content: [
|
|
820
|
-
{
|
|
821
|
-
type: "text",
|
|
822
|
-
text: JSON.stringify({
|
|
823
|
-
status: "started",
|
|
824
|
-
processId: processId,
|
|
825
|
-
pid: devProcess.pid,
|
|
826
|
-
command: command,
|
|
827
|
-
cwd: cwd,
|
|
828
|
-
outputFile: outputFile,
|
|
829
|
-
message: `Dev server started with process ID: ${processId}. Logs streaming to ${outputFile}`
|
|
830
|
-
}, null, 2)
|
|
831
|
-
}
|
|
832
|
-
]
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
catch (error) {
|
|
836
|
-
throw new Error(`Failed to start dev server: ${error instanceof Error ? error.message : String(error)}`);
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
case "dev_get_processes": {
|
|
840
|
-
const validatedArgs = GetProcessesArgsSchema.parse(args);
|
|
841
|
-
const format = validatedArgs.format || "formatted";
|
|
842
|
-
if (validatedArgs.processId) {
|
|
843
|
-
// Get info for specific process
|
|
844
|
-
const serverInfo = this.activeServers.get(validatedArgs.processId);
|
|
845
|
-
if (!serverInfo) {
|
|
846
|
-
return {
|
|
847
|
-
content: [
|
|
848
|
-
{
|
|
849
|
-
type: "text",
|
|
850
|
-
text: JSON.stringify({
|
|
851
|
-
status: "not_found",
|
|
852
|
-
message: `Process with ID '${validatedArgs.processId}' not found`
|
|
853
|
-
}, null, 2)
|
|
854
|
-
}
|
|
855
|
-
]
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
const uptime = new Date().getTime() - serverInfo.startTime.getTime();
|
|
859
|
-
const processInfo = {
|
|
860
|
-
processId: serverInfo.processId,
|
|
861
|
-
pid: serverInfo.pid,
|
|
862
|
-
command: serverInfo.command,
|
|
863
|
-
cwd: serverInfo.cwd,
|
|
864
|
-
outputFile: serverInfo.outputFile,
|
|
865
|
-
startTime: serverInfo.startTime,
|
|
866
|
-
uptime: format === "raw" ? uptime : {
|
|
867
|
-
hours: Math.floor(uptime / 3600000),
|
|
868
|
-
minutes: Math.floor((uptime % 3600000) / 60000),
|
|
869
|
-
seconds: Math.floor((uptime % 60000) / 1000)
|
|
870
|
-
}
|
|
871
|
-
};
|
|
872
|
-
return {
|
|
873
|
-
content: [
|
|
874
|
-
{
|
|
875
|
-
type: "text",
|
|
876
|
-
text: JSON.stringify({
|
|
877
|
-
status: "running",
|
|
878
|
-
process: processInfo
|
|
879
|
-
}, null, 2)
|
|
880
|
-
}
|
|
881
|
-
]
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
// Get info for all processes
|
|
886
|
-
if (this.activeServers.size === 0) {
|
|
887
|
-
return {
|
|
888
|
-
content: [
|
|
889
|
-
{
|
|
890
|
-
type: "text",
|
|
891
|
-
text: JSON.stringify({
|
|
892
|
-
status: "not_running",
|
|
893
|
-
message: "No dev servers are currently running"
|
|
894
|
-
}, null, 2)
|
|
895
|
-
}
|
|
896
|
-
]
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
const processes = Array.from(this.activeServers.values()).map(serverInfo => {
|
|
900
|
-
const uptime = new Date().getTime() - serverInfo.startTime.getTime();
|
|
901
|
-
return {
|
|
902
|
-
processId: serverInfo.processId,
|
|
903
|
-
pid: serverInfo.pid,
|
|
904
|
-
command: serverInfo.command,
|
|
905
|
-
cwd: serverInfo.cwd,
|
|
906
|
-
outputFile: serverInfo.outputFile,
|
|
907
|
-
startTime: serverInfo.startTime,
|
|
908
|
-
uptime: format === "raw" ? uptime : {
|
|
909
|
-
hours: Math.floor(uptime / 3600000),
|
|
910
|
-
minutes: Math.floor((uptime % 3600000) / 60000),
|
|
911
|
-
seconds: Math.floor((uptime % 60000) / 1000)
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
});
|
|
915
|
-
return {
|
|
916
|
-
content: [
|
|
917
|
-
{
|
|
918
|
-
type: "text",
|
|
919
|
-
text: JSON.stringify({
|
|
920
|
-
status: "running",
|
|
921
|
-
totalProcesses: processes.length,
|
|
922
|
-
processes: processes
|
|
923
|
-
}, null, 2)
|
|
924
|
-
}
|
|
925
|
-
]
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
case "dev_stop_log_streaming": {
|
|
930
|
-
const validatedArgs = StopLogStreamingArgsSchema.parse(args);
|
|
931
|
-
if (validatedArgs.processId) {
|
|
932
|
-
// Stop specific process
|
|
933
|
-
const serverInfo = this.activeServers.get(validatedArgs.processId);
|
|
934
|
-
if (!serverInfo) {
|
|
935
|
-
return {
|
|
936
|
-
content: [
|
|
937
|
-
{
|
|
938
|
-
type: "text",
|
|
939
|
-
text: JSON.stringify({
|
|
940
|
-
status: "not_found",
|
|
941
|
-
message: `Process with ID '${validatedArgs.processId}' not found`
|
|
942
|
-
}, null, 2)
|
|
943
|
-
}
|
|
944
|
-
]
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
try {
|
|
948
|
-
// Close browser if running
|
|
949
|
-
if (serverInfo.browser) {
|
|
950
|
-
await serverInfo.browser.close();
|
|
951
|
-
}
|
|
952
|
-
// Remove all event listeners to prevent memory leaks
|
|
953
|
-
if (serverInfo.process) {
|
|
954
|
-
serverInfo.process.stdout?.removeAllListeners('data');
|
|
955
|
-
serverInfo.process.stderr?.removeAllListeners('data');
|
|
956
|
-
serverInfo.process.removeAllListeners('exit');
|
|
957
|
-
serverInfo.process.removeAllListeners('error');
|
|
958
|
-
}
|
|
959
|
-
if (serverInfo.pid) {
|
|
960
|
-
process.kill(serverInfo.pid, 'SIGTERM');
|
|
961
|
-
}
|
|
962
|
-
this.activeServers.delete(validatedArgs.processId);
|
|
963
|
-
this.saveState();
|
|
964
|
-
return {
|
|
965
|
-
content: [
|
|
966
|
-
{
|
|
967
|
-
type: "text",
|
|
968
|
-
text: JSON.stringify({
|
|
969
|
-
status: "stopped",
|
|
970
|
-
processId: validatedArgs.processId,
|
|
971
|
-
message: `Process '${validatedArgs.processId}' stopped successfully`
|
|
972
|
-
}, null, 2)
|
|
973
|
-
}
|
|
974
|
-
]
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
catch (error) {
|
|
978
|
-
throw new Error(`Failed to stop process '${validatedArgs.processId}': ${error instanceof Error ? error.message : String(error)}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
else {
|
|
982
|
-
// Stop all processes
|
|
983
|
-
if (this.activeServers.size === 0) {
|
|
984
|
-
return {
|
|
985
|
-
content: [
|
|
986
|
-
{
|
|
987
|
-
type: "text",
|
|
988
|
-
text: JSON.stringify({
|
|
989
|
-
status: "not_running",
|
|
990
|
-
message: "No dev servers are currently running"
|
|
991
|
-
}, null, 2)
|
|
992
|
-
}
|
|
993
|
-
]
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
const stoppedProcesses = [];
|
|
997
|
-
for (const [processId, serverInfo] of this.activeServers) {
|
|
998
|
-
try {
|
|
999
|
-
// Close browser if running
|
|
1000
|
-
if (serverInfo.browser) {
|
|
1001
|
-
await serverInfo.browser.close();
|
|
1002
|
-
}
|
|
1003
|
-
// Remove all event listeners to prevent memory leaks
|
|
1004
|
-
if (serverInfo.process) {
|
|
1005
|
-
serverInfo.process.stdout?.removeAllListeners('data');
|
|
1006
|
-
serverInfo.process.stderr?.removeAllListeners('data');
|
|
1007
|
-
serverInfo.process.removeAllListeners('exit');
|
|
1008
|
-
serverInfo.process.removeAllListeners('error');
|
|
1009
|
-
}
|
|
1010
|
-
if (serverInfo.pid) {
|
|
1011
|
-
process.kill(serverInfo.pid, 'SIGTERM');
|
|
1012
|
-
}
|
|
1013
|
-
stoppedProcesses.push(processId);
|
|
1014
|
-
}
|
|
1015
|
-
catch (error) {
|
|
1016
|
-
// Continue with other processes even if one fails
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
this.activeServers.clear();
|
|
1020
|
-
this.saveState();
|
|
1021
|
-
return {
|
|
1022
|
-
content: [
|
|
1023
|
-
{
|
|
1024
|
-
type: "text",
|
|
1025
|
-
text: JSON.stringify({
|
|
1026
|
-
status: "stopped",
|
|
1027
|
-
stoppedProcesses: stoppedProcesses,
|
|
1028
|
-
message: `Stopped ${stoppedProcesses.length} processes`
|
|
1029
|
-
}, null, 2)
|
|
1030
|
-
}
|
|
1031
|
-
]
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
case "dev_restart_log_streaming": {
|
|
1036
|
-
const validatedArgs = RestartLogStreamingArgsSchema.parse(args);
|
|
1037
|
-
const processId = validatedArgs.processId;
|
|
1038
|
-
const serverInfo = this.activeServers.get(processId);
|
|
1039
|
-
if (!serverInfo) {
|
|
1040
|
-
return {
|
|
1041
|
-
content: [
|
|
1042
|
-
{
|
|
1043
|
-
type: "text",
|
|
1044
|
-
text: JSON.stringify({
|
|
1045
|
-
status: "not_found",
|
|
1046
|
-
message: `Process with ID '${processId}' not found. Use dev_list_processes to see available processes.`
|
|
1047
|
-
}, null, 2)
|
|
1048
|
-
}
|
|
1049
|
-
]
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
// Save current configuration
|
|
1053
|
-
const { command, cwd, outputFile } = serverInfo;
|
|
1054
|
-
// Stop current process
|
|
1055
|
-
try {
|
|
1056
|
-
// Remove all event listeners to prevent memory leaks
|
|
1057
|
-
if (serverInfo.process) {
|
|
1058
|
-
serverInfo.process.stdout?.removeAllListeners('data');
|
|
1059
|
-
serverInfo.process.stderr?.removeAllListeners('data');
|
|
1060
|
-
serverInfo.process.removeAllListeners('exit');
|
|
1061
|
-
serverInfo.process.removeAllListeners('error');
|
|
1062
|
-
}
|
|
1063
|
-
if (serverInfo.pid) {
|
|
1064
|
-
process.kill(serverInfo.pid, 'SIGTERM');
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
catch (error) {
|
|
1068
|
-
// Process might already be dead
|
|
1069
|
-
}
|
|
1070
|
-
// Clear logs if requested
|
|
1071
|
-
if (validatedArgs.clearLogs && existsSync(outputFile)) {
|
|
1072
|
-
writeFileSync(outputFile, '');
|
|
1073
|
-
}
|
|
1074
|
-
// Remove the old server info to ensure complete cleanup
|
|
1075
|
-
this.activeServers.delete(processId);
|
|
1076
|
-
// Wait a moment for process to die
|
|
1077
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1078
|
-
// Start new process
|
|
1079
|
-
const [program, ...cmdArgs] = command.split(' ');
|
|
1080
|
-
const restartMessage = `[${new Date().toISOString()}] Restarting: ${command} (Process ID: ${processId})\n`;
|
|
1081
|
-
if (validatedArgs.clearLogs) {
|
|
1082
|
-
writeFileSync(outputFile, restartMessage);
|
|
1083
|
-
}
|
|
1084
|
-
else {
|
|
1085
|
-
appendFileSync(outputFile, restartMessage);
|
|
1086
|
-
}
|
|
1087
|
-
const devProcess = spawn(program, cmdArgs, {
|
|
1088
|
-
cwd: cwd,
|
|
1089
|
-
env: process.env,
|
|
1090
|
-
detached: true,
|
|
1091
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
1092
|
-
});
|
|
1093
|
-
const newServerInfo = {
|
|
1094
|
-
process: devProcess,
|
|
1095
|
-
command: command,
|
|
1096
|
-
cwd: cwd,
|
|
1097
|
-
outputFile: outputFile,
|
|
1098
|
-
startTime: new Date(),
|
|
1099
|
-
pid: devProcess.pid,
|
|
1100
|
-
processId: processId
|
|
1101
|
-
};
|
|
1102
|
-
this.activeServers.set(processId, newServerInfo);
|
|
1103
|
-
// Stream stdout to file
|
|
1104
|
-
devProcess.stdout?.on('data', (data) => {
|
|
1105
|
-
const timestamp = new Date().toISOString();
|
|
1106
|
-
const logEntry = `[${timestamp}] [${processId}] ${data}`;
|
|
1107
|
-
appendFileSync(outputFile, logEntry);
|
|
1108
|
-
});
|
|
1109
|
-
// Stream stderr to file
|
|
1110
|
-
devProcess.stderr?.on('data', (data) => {
|
|
1111
|
-
const timestamp = new Date().toISOString();
|
|
1112
|
-
const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
|
|
1113
|
-
appendFileSync(outputFile, logEntry);
|
|
1114
|
-
});
|
|
1115
|
-
// Handle process exit
|
|
1116
|
-
devProcess.on('exit', (code, signal) => {
|
|
1117
|
-
const timestamp = new Date().toISOString();
|
|
1118
|
-
const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
|
|
1119
|
-
appendFileSync(outputFile, exitMessage);
|
|
1120
|
-
this.activeServers.delete(processId);
|
|
1121
|
-
this.saveState();
|
|
1122
|
-
});
|
|
1123
|
-
// Handle process errors
|
|
1124
|
-
devProcess.on('error', (error) => {
|
|
1125
|
-
const timestamp = new Date().toISOString();
|
|
1126
|
-
const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
|
|
1127
|
-
appendFileSync(outputFile, errorMessage);
|
|
1128
|
-
});
|
|
1129
|
-
this.saveState();
|
|
1130
|
-
return {
|
|
1131
|
-
content: [
|
|
1132
|
-
{
|
|
1133
|
-
type: "text",
|
|
1134
|
-
text: JSON.stringify({
|
|
1135
|
-
status: "restarted",
|
|
1136
|
-
processId: processId,
|
|
1137
|
-
pid: devProcess.pid,
|
|
1138
|
-
command: command,
|
|
1139
|
-
outputFile: outputFile,
|
|
1140
|
-
message: `Process '${processId}' restarted successfully`
|
|
1141
|
-
}, null, 2)
|
|
1142
|
-
}
|
|
1143
|
-
]
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
case "dev_tail_logs": {
|
|
1147
|
-
const validatedArgs = TailLogsArgsSchema.parse(args);
|
|
1148
|
-
// Find the process or use the default behavior
|
|
1149
|
-
const processId = this.findProcessOrDefault(validatedArgs.processId);
|
|
1150
|
-
if (!processId && validatedArgs.processId) {
|
|
1151
|
-
return {
|
|
1152
|
-
content: [
|
|
1153
|
-
{
|
|
1154
|
-
type: "text",
|
|
1155
|
-
text: JSON.stringify({
|
|
1156
|
-
status: "not_found",
|
|
1157
|
-
message: `Process with ID '${validatedArgs.processId}' not found`
|
|
1158
|
-
}, null, 2)
|
|
1159
|
-
}
|
|
1160
|
-
]
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
if (!processId && this.activeServers.size > 1) {
|
|
1164
|
-
return {
|
|
1165
|
-
content: [
|
|
1166
|
-
{
|
|
1167
|
-
type: "text",
|
|
1168
|
-
text: JSON.stringify({
|
|
1169
|
-
status: "ambiguous",
|
|
1170
|
-
message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
|
|
1171
|
-
availableProcesses: Array.from(this.activeServers.keys())
|
|
1172
|
-
}, null, 2)
|
|
1173
|
-
}
|
|
1174
|
-
]
|
|
1175
|
-
};
|
|
1176
|
-
}
|
|
1177
|
-
// Get the output file
|
|
1178
|
-
let outputFile;
|
|
1179
|
-
if (processId) {
|
|
1180
|
-
const serverInfo = this.activeServers.get(processId);
|
|
1181
|
-
outputFile = serverInfo.outputFile;
|
|
1182
|
-
}
|
|
1183
|
-
else {
|
|
1184
|
-
outputFile = "dev-server-logs.txt"; // fallback
|
|
1185
|
-
}
|
|
1186
|
-
if (!existsSync(outputFile)) {
|
|
1187
|
-
return {
|
|
1188
|
-
content: [
|
|
1189
|
-
{
|
|
1190
|
-
type: "text",
|
|
1191
|
-
text: JSON.stringify({
|
|
1192
|
-
status: "error",
|
|
1193
|
-
message: `Log file not found: ${outputFile}`
|
|
1194
|
-
}, null, 2)
|
|
1195
|
-
}
|
|
1196
|
-
]
|
|
1197
|
-
};
|
|
1198
|
-
}
|
|
1199
|
-
try {
|
|
1200
|
-
// Read the file
|
|
1201
|
-
const content = readFileSync(outputFile, 'utf8');
|
|
1202
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
1203
|
-
// Filter if pattern provided
|
|
1204
|
-
let filteredLines = lines;
|
|
1205
|
-
if (validatedArgs.filter) {
|
|
1206
|
-
const pattern = new RegExp(validatedArgs.filter, 'i');
|
|
1207
|
-
filteredLines = lines.filter(line => pattern.test(line));
|
|
1208
|
-
}
|
|
1209
|
-
// Get last N lines
|
|
1210
|
-
const numLines = validatedArgs.lines || 50;
|
|
1211
|
-
const lastLines = filteredLines.slice(-numLines);
|
|
1212
|
-
return {
|
|
1213
|
-
content: [
|
|
1214
|
-
{
|
|
1215
|
-
type: "text",
|
|
1216
|
-
text: JSON.stringify({
|
|
1217
|
-
status: "success",
|
|
1218
|
-
processId: processId || "default",
|
|
1219
|
-
file: outputFile,
|
|
1220
|
-
totalLines: lines.length,
|
|
1221
|
-
filteredLines: filteredLines.length,
|
|
1222
|
-
returnedLines: lastLines.length,
|
|
1223
|
-
logs: lastLines
|
|
1224
|
-
}, null, 2)
|
|
1225
|
-
}
|
|
1226
|
-
]
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
catch (error) {
|
|
1230
|
-
throw new Error(`Failed to read logs: ${error instanceof Error ? error.message : String(error)}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
case "dev_clear_logs": {
|
|
1234
|
-
const validatedArgs = ClearLogsArgsSchema.parse(args);
|
|
1235
|
-
// Find the process or use the default behavior
|
|
1236
|
-
const processId = this.findProcessOrDefault(validatedArgs.processId);
|
|
1237
|
-
if (!processId && validatedArgs.processId) {
|
|
1238
|
-
return {
|
|
1239
|
-
content: [
|
|
1240
|
-
{
|
|
1241
|
-
type: "text",
|
|
1242
|
-
text: JSON.stringify({
|
|
1243
|
-
status: "not_found",
|
|
1244
|
-
message: `Process with ID '${validatedArgs.processId}' not found`
|
|
1245
|
-
}, null, 2)
|
|
1246
|
-
}
|
|
1247
|
-
]
|
|
1248
|
-
};
|
|
1249
|
-
}
|
|
1250
|
-
if (!processId && this.activeServers.size > 1) {
|
|
1251
|
-
return {
|
|
1252
|
-
content: [
|
|
1253
|
-
{
|
|
1254
|
-
type: "text",
|
|
1255
|
-
text: JSON.stringify({
|
|
1256
|
-
status: "ambiguous",
|
|
1257
|
-
message: "Multiple processes running. Please specify processId or use dev_list_processes to see options.",
|
|
1258
|
-
availableProcesses: Array.from(this.activeServers.keys())
|
|
1259
|
-
}, null, 2)
|
|
1260
|
-
}
|
|
1261
|
-
]
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
// Get the output file
|
|
1265
|
-
let outputFile;
|
|
1266
|
-
if (processId) {
|
|
1267
|
-
const serverInfo = this.activeServers.get(processId);
|
|
1268
|
-
outputFile = serverInfo.outputFile;
|
|
1269
|
-
}
|
|
1270
|
-
else {
|
|
1271
|
-
outputFile = "dev-server-logs.txt"; // fallback
|
|
1272
|
-
}
|
|
1273
|
-
if (!existsSync(outputFile)) {
|
|
1274
|
-
return {
|
|
1275
|
-
content: [
|
|
1276
|
-
{
|
|
1277
|
-
type: "text",
|
|
1278
|
-
text: JSON.stringify({
|
|
1279
|
-
status: "error",
|
|
1280
|
-
message: `Log file not found: ${outputFile}`
|
|
1281
|
-
}, null, 2)
|
|
1282
|
-
}
|
|
1283
|
-
]
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
try {
|
|
1287
|
-
// Backup if requested
|
|
1288
|
-
if (validatedArgs.backup) {
|
|
1289
|
-
const backupFile = `${outputFile}.${new Date().getTime()}.backup`;
|
|
1290
|
-
copyFileSync(outputFile, backupFile);
|
|
1291
|
-
}
|
|
1292
|
-
// Clear the file
|
|
1293
|
-
writeFileSync(outputFile, '');
|
|
1294
|
-
return {
|
|
1295
|
-
content: [
|
|
1296
|
-
{
|
|
1297
|
-
type: "text",
|
|
1298
|
-
text: JSON.stringify({
|
|
1299
|
-
status: "success",
|
|
1300
|
-
processId: processId || "default",
|
|
1301
|
-
message: `Log file cleared: ${outputFile}`,
|
|
1302
|
-
backup: validatedArgs.backup ? "Created backup" : "No backup created"
|
|
1303
|
-
}, null, 2)
|
|
1304
|
-
}
|
|
1305
|
-
]
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
catch (error) {
|
|
1309
|
-
throw new Error(`Failed to clear logs: ${error instanceof Error ? error.message : String(error)}`);
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
case "dev_check_running_processes": {
|
|
1313
|
-
const validatedArgs = CheckRunningProcessesArgsSchema.parse(args);
|
|
1314
|
-
try {
|
|
1315
|
-
const result = await this.checkRunningProcesses(validatedArgs.processType, validatedArgs.port);
|
|
1316
|
-
return {
|
|
1317
|
-
content: [
|
|
1318
|
-
{
|
|
1319
|
-
type: "text",
|
|
1320
|
-
text: JSON.stringify(result, null, 2)
|
|
1321
|
-
}
|
|
1322
|
-
]
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
catch (error) {
|
|
1326
|
-
throw new Error(`Failed to check running processes: ${error instanceof Error ? error.message : String(error)}`);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
case "dev_discover_logs": {
|
|
1330
|
-
const validatedArgs = DiscoverLogsArgsSchema.parse(args);
|
|
1331
|
-
try {
|
|
1332
|
-
const result = await this.discoverLogs(validatedArgs.sessionDate);
|
|
1333
|
-
return {
|
|
1334
|
-
content: [
|
|
1335
|
-
{
|
|
1336
|
-
type: "text",
|
|
1337
|
-
text: JSON.stringify(result, null, 2)
|
|
1338
|
-
}
|
|
1339
|
-
]
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
catch (error) {
|
|
1343
|
-
throw new Error(`Failed to discover logs: ${error instanceof Error ? error.message : String(error)}`);
|
|
1344
|
-
}
|
|
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(1920),
|
|
1353
|
-
height: z.number().default(1080)
|
|
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: 1920, height: 1080 };
|
|
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 (hydration-safe version)
|
|
1536
|
-
if (teachingMode) {
|
|
1537
|
-
// @ts-ignore
|
|
1538
|
-
await page.evaluate(() => {
|
|
1539
|
-
function injectTestBanner() {
|
|
1540
|
-
// Create a container that won't interfere with React hydration
|
|
1541
|
-
const container = document.createElement('div');
|
|
1542
|
-
container.id = 'test-browser-banner-container';
|
|
1543
|
-
container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; z-index: 2147483647; pointer-events: none;';
|
|
1544
|
-
// Use shadow DOM to isolate the banner completely
|
|
1545
|
-
const shadow = container.attachShadow({ mode: 'open' });
|
|
1546
|
-
// Create banner in shadow DOM
|
|
1547
|
-
const banner = document.createElement('div');
|
|
1548
|
-
banner.style.cssText = `
|
|
1549
|
-
position: fixed;
|
|
1550
|
-
top: 0;
|
|
1551
|
-
left: 0;
|
|
1552
|
-
right: 0;
|
|
1553
|
-
background: #4CAF50;
|
|
1554
|
-
color: white;
|
|
1555
|
-
padding: 10px;
|
|
1556
|
-
text-align: center;
|
|
1557
|
-
font-family: monospace;
|
|
1558
|
-
font-size: 14px;
|
|
1559
|
-
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
1560
|
-
pointer-events: auto;
|
|
1561
|
-
`;
|
|
1562
|
-
banner.innerHTML = `
|
|
1563
|
-
🧪 TEST BROWSER - All console logs are being captured!
|
|
1564
|
-
<button style="
|
|
1565
|
-
margin-left: 20px;
|
|
1566
|
-
background: transparent;
|
|
1567
|
-
border: 1px solid white;
|
|
1568
|
-
color: white;
|
|
1569
|
-
padding: 2px 10px;
|
|
1570
|
-
cursor: pointer;
|
|
1571
|
-
">Hide</button>
|
|
1572
|
-
`;
|
|
1573
|
-
// Add click handler to hide button
|
|
1574
|
-
const button = banner.querySelector('button');
|
|
1575
|
-
if (button) {
|
|
1576
|
-
button.addEventListener('click', () => {
|
|
1577
|
-
container.style.display = 'none';
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
shadow.appendChild(banner);
|
|
1581
|
-
document.documentElement.appendChild(container);
|
|
1582
|
-
}
|
|
1583
|
-
// Wait for hydration to complete before injecting
|
|
1584
|
-
if (typeof window !== 'undefined') {
|
|
1585
|
-
// Check for Next.js hydration
|
|
1586
|
-
if (window.__NEXT_HYDRATED) {
|
|
1587
|
-
injectTestBanner();
|
|
1588
|
-
}
|
|
1589
|
-
else {
|
|
1590
|
-
// Wait for React to finish hydrating
|
|
1591
|
-
let attempts = 0;
|
|
1592
|
-
const checkHydration = setInterval(() => {
|
|
1593
|
-
attempts++;
|
|
1594
|
-
// Check various hydration indicators
|
|
1595
|
-
if (window.__NEXT_HYDRATED || window._react_root || document.querySelector('[data-reactroot]') || attempts > 20) {
|
|
1596
|
-
clearInterval(checkHydration);
|
|
1597
|
-
// Add a small delay to be extra safe
|
|
1598
|
-
setTimeout(injectTestBanner, 100);
|
|
1599
|
-
}
|
|
1600
|
-
}, 250);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}).catch(() => { });
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
catch (error) {
|
|
1607
|
-
const timestamp = new Date().toISOString();
|
|
1608
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NAVIGATION ERROR] Failed to navigate to ${browserUrl}: ${error}\n`;
|
|
1609
|
-
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1610
|
-
}
|
|
1611
|
-
// Update server info
|
|
1612
|
-
serverInfo.browser = browser;
|
|
1613
|
-
serverInfo.page = page;
|
|
1614
|
-
serverInfo.browserUrl = browserUrl;
|
|
1615
|
-
serverInfo.consoleCapture = true;
|
|
1616
|
-
// Log browser start
|
|
1617
|
-
const timestamp = new Date().toISOString();
|
|
1618
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`;
|
|
1619
|
-
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1620
|
-
return {
|
|
1621
|
-
content: [
|
|
1622
|
-
{
|
|
1623
|
-
type: "text",
|
|
1624
|
-
text: JSON.stringify({
|
|
1625
|
-
status: "started",
|
|
1626
|
-
processId: processId,
|
|
1627
|
-
browserUrl: browserUrl,
|
|
1628
|
-
teachingMode: teachingMode,
|
|
1629
|
-
message: `🧪 TEST BROWSER LAUNCHED!\n\n` +
|
|
1630
|
-
`👉 IMPORTANT: Students should use THIS browser window (not their regular browser)\n` +
|
|
1631
|
-
`📝 All console logs are being captured to: ${serverInfo.outputFile}\n` +
|
|
1632
|
-
`🔍 DevTools is ${teachingMode ? 'OPEN' : 'CLOSED'} for debugging\n` +
|
|
1633
|
-
`⚠️ Errors will ${highlightErrors ? 'flash red' : 'not flash'} on screen\n\n` +
|
|
1634
|
-
`Students can now interact with the app at: ${browserUrl}`
|
|
1635
|
-
}, null, 2)
|
|
1636
|
-
}
|
|
1637
|
-
]
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
catch (error) {
|
|
1641
|
-
throw new Error(`Failed to start browser console capture: ${error instanceof Error ? error.message : String(error)}`);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
case "dev_stop_browser_console": {
|
|
1645
|
-
const validatedArgs = z.object({
|
|
1646
|
-
processId: z.string().optional()
|
|
1647
|
-
}).parse(args);
|
|
1648
|
-
try {
|
|
1649
|
-
const processId = this.findProcessOrDefault(validatedArgs.processId);
|
|
1650
|
-
if (!processId) {
|
|
1651
|
-
return {
|
|
1652
|
-
content: [
|
|
1653
|
-
{
|
|
1654
|
-
type: "text",
|
|
1655
|
-
text: JSON.stringify({
|
|
1656
|
-
status: "error",
|
|
1657
|
-
message: "No process found"
|
|
1658
|
-
}, null, 2)
|
|
1659
|
-
}
|
|
1660
|
-
]
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
const serverInfo = this.activeServers.get(processId);
|
|
1664
|
-
if (!serverInfo || !serverInfo.browser) {
|
|
1665
|
-
return {
|
|
1666
|
-
content: [
|
|
1667
|
-
{
|
|
1668
|
-
type: "text",
|
|
1669
|
-
text: JSON.stringify({
|
|
1670
|
-
status: "not_running",
|
|
1671
|
-
message: `No browser console capture running for process '${processId}'`
|
|
1672
|
-
}, null, 2)
|
|
1673
|
-
}
|
|
1674
|
-
]
|
|
1675
|
-
};
|
|
1676
|
-
}
|
|
1677
|
-
// Close browser
|
|
1678
|
-
await serverInfo.browser.close();
|
|
1679
|
-
// Clear browser info
|
|
1680
|
-
serverInfo.browser = undefined;
|
|
1681
|
-
serverInfo.page = undefined;
|
|
1682
|
-
serverInfo.browserUrl = undefined;
|
|
1683
|
-
serverInfo.consoleCapture = false;
|
|
1684
|
-
// Log browser stop
|
|
1685
|
-
const timestamp = new Date().toISOString();
|
|
1686
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] Console capture stopped\n`;
|
|
1687
|
-
appendFileSync(serverInfo.outputFile, logEntry);
|
|
1688
|
-
return {
|
|
1689
|
-
content: [
|
|
1690
|
-
{
|
|
1691
|
-
type: "text",
|
|
1692
|
-
text: JSON.stringify({
|
|
1693
|
-
status: "stopped",
|
|
1694
|
-
processId: processId,
|
|
1695
|
-
message: `Browser console capture stopped for process '${processId}'`
|
|
1696
|
-
}, null, 2)
|
|
1697
|
-
}
|
|
1698
|
-
]
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
catch (error) {
|
|
1702
|
-
throw new Error(`Failed to stop browser console capture: ${error instanceof Error ? error.message : String(error)}`);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
case "dev_start_frontend_with_browser": {
|
|
1706
|
-
const validatedArgs = StartFrontendWithBrowserArgsSchema.parse(args);
|
|
1707
|
-
// Defaults
|
|
1708
|
-
const command = validatedArgs.command || "npm run dev";
|
|
1709
|
-
const processId = validatedArgs.processId || "frontend-with-browser";
|
|
1710
|
-
const cwd = resolve(validatedArgs.cwd || process.cwd());
|
|
1711
|
-
const waitTimeout = validatedArgs.waitTimeout || 30000;
|
|
1712
|
-
const browserDelay = validatedArgs.browserDelay || 1000;
|
|
1713
|
-
const teachingMode = validatedArgs.teachingMode !== false; // default true
|
|
1714
|
-
try {
|
|
1715
|
-
// Check if process already exists
|
|
1716
|
-
if (this.activeServers.has(processId)) {
|
|
1717
|
-
throw new Error(`Process '${processId}' is already running. Stop it first or use a different processId.`);
|
|
1718
|
-
}
|
|
1719
|
-
// Step 1: Start the dev server
|
|
1720
|
-
const outputFile = this.createStructuredLogPath(command, undefined, "frontend");
|
|
1721
|
-
this.cleanupOldLogs(3);
|
|
1722
|
-
const [program, ...cmdArgs] = command.split(' ');
|
|
1723
|
-
// Initial log entry
|
|
1724
|
-
writeFileSync(outputFile, `[${new Date().toISOString()}] Starting: ${command} (Process ID: ${processId})\n`);
|
|
1725
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Starting development server...\n`);
|
|
1726
|
-
// Spawn the process
|
|
1727
|
-
const devProcess = spawn(program, cmdArgs, {
|
|
1728
|
-
cwd: cwd,
|
|
1729
|
-
env: { ...process.env, ...validatedArgs.env },
|
|
1730
|
-
detached: true,
|
|
1731
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
1732
|
-
});
|
|
1733
|
-
const serverInfo = {
|
|
1734
|
-
process: devProcess,
|
|
1735
|
-
command: command,
|
|
1736
|
-
cwd: cwd,
|
|
1737
|
-
outputFile: outputFile,
|
|
1738
|
-
startTime: new Date(),
|
|
1739
|
-
pid: devProcess.pid,
|
|
1740
|
-
processId: processId
|
|
1741
|
-
};
|
|
1742
|
-
this.activeServers.set(processId, serverInfo);
|
|
1743
|
-
// Variables for port detection
|
|
1744
|
-
let detectedPort = validatedArgs.port || null;
|
|
1745
|
-
let serverReady = false;
|
|
1746
|
-
let outputBuffer = "";
|
|
1747
|
-
// Common port patterns
|
|
1748
|
-
const portPatterns = [
|
|
1749
|
-
/(?:Local|Listening|ready).*?(\d{4})/i,
|
|
1750
|
-
/localhost:(\d{4})/,
|
|
1751
|
-
/port\s*:?\s*(\d{4})/i,
|
|
1752
|
-
/on\s+http:\/\/localhost:(\d{4})/i,
|
|
1753
|
-
/Server running at.*?:(\d{4})/i
|
|
1754
|
-
];
|
|
1755
|
-
// Ready indicators
|
|
1756
|
-
const readyPatterns = [
|
|
1757
|
-
/Local:\s*http:\/\/localhost/i,
|
|
1758
|
-
/ready on http:\/\/localhost/i,
|
|
1759
|
-
/Server running at/i,
|
|
1760
|
-
/Listening on port/i,
|
|
1761
|
-
/started server on/i,
|
|
1762
|
-
/Ready in \d+ms/i,
|
|
1763
|
-
/compiled successfully/i
|
|
1764
|
-
];
|
|
1765
|
-
// Create promise for server ready detection
|
|
1766
|
-
const serverReadyPromise = new Promise((resolve, reject) => {
|
|
1767
|
-
const timeout = setTimeout(() => {
|
|
1768
|
-
reject(new Error(`Server did not start within ${waitTimeout}ms`));
|
|
1769
|
-
}, waitTimeout);
|
|
1770
|
-
const checkOutput = (data) => {
|
|
1771
|
-
const text = data.toString();
|
|
1772
|
-
outputBuffer += text;
|
|
1773
|
-
// Check for port if not detected
|
|
1774
|
-
if (!detectedPort) {
|
|
1775
|
-
for (const pattern of portPatterns) {
|
|
1776
|
-
const match = outputBuffer.match(pattern);
|
|
1777
|
-
if (match && match[1]) {
|
|
1778
|
-
detectedPort = parseInt(match[1]);
|
|
1779
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] 🔍 Detected port: ${detectedPort}\n`);
|
|
1780
|
-
break;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
|
-
// Check for ready state
|
|
1785
|
-
if (!serverReady) {
|
|
1786
|
-
for (const pattern of readyPatterns) {
|
|
1787
|
-
if (pattern.test(outputBuffer)) {
|
|
1788
|
-
serverReady = true;
|
|
1789
|
-
clearTimeout(timeout);
|
|
1790
|
-
// If we have a port, resolve immediately
|
|
1791
|
-
if (detectedPort) {
|
|
1792
|
-
resolve(detectedPort);
|
|
1793
|
-
}
|
|
1794
|
-
else {
|
|
1795
|
-
// Try common ports
|
|
1796
|
-
const commonPorts = [3000, 3001, 5173, 8080, 4200];
|
|
1797
|
-
detectedPort = commonPorts[0]; // Default to 3000
|
|
1798
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⚠️ Could not detect port, defaulting to ${detectedPort}\n`);
|
|
1799
|
-
resolve(detectedPort);
|
|
1800
|
-
}
|
|
1801
|
-
break;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
};
|
|
1806
|
-
// Attach listeners
|
|
1807
|
-
devProcess.stdout?.on('data', checkOutput);
|
|
1808
|
-
devProcess.stderr?.on('data', checkOutput);
|
|
1809
|
-
});
|
|
1810
|
-
// Stream stdout to file
|
|
1811
|
-
devProcess.stdout?.on('data', (data) => {
|
|
1812
|
-
const timestamp = new Date().toISOString();
|
|
1813
|
-
const logEntry = `[${timestamp}] [${processId}] ${data}`;
|
|
1814
|
-
appendFileSync(outputFile, logEntry);
|
|
1815
|
-
});
|
|
1816
|
-
// Stream stderr to file
|
|
1817
|
-
devProcess.stderr?.on('data', (data) => {
|
|
1818
|
-
const timestamp = new Date().toISOString();
|
|
1819
|
-
const logEntry = `[${timestamp}] [${processId}] [ERROR] ${data}`;
|
|
1820
|
-
appendFileSync(outputFile, logEntry);
|
|
1821
|
-
});
|
|
1822
|
-
// Handle process exit
|
|
1823
|
-
devProcess.on('exit', (code, signal) => {
|
|
1824
|
-
const timestamp = new Date().toISOString();
|
|
1825
|
-
const exitMessage = `[${timestamp}] [${processId}] Process exited with code ${code} and signal ${signal}\n`;
|
|
1826
|
-
appendFileSync(outputFile, exitMessage);
|
|
1827
|
-
this.activeServers.delete(processId);
|
|
1828
|
-
this.saveState();
|
|
1829
|
-
});
|
|
1830
|
-
// Handle process errors
|
|
1831
|
-
devProcess.on('error', (error) => {
|
|
1832
|
-
const timestamp = new Date().toISOString();
|
|
1833
|
-
const errorMessage = `[${timestamp}] [${processId}] Process error: ${error.message}\n`;
|
|
1834
|
-
appendFileSync(outputFile, errorMessage);
|
|
1835
|
-
});
|
|
1836
|
-
this.saveState();
|
|
1837
|
-
// Step 2: Wait for server to be ready
|
|
1838
|
-
let port;
|
|
1839
|
-
try {
|
|
1840
|
-
port = await serverReadyPromise;
|
|
1841
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Server ready on http://localhost:${port}\n`);
|
|
1842
|
-
}
|
|
1843
|
-
catch (error) {
|
|
1844
|
-
// Server didn't start properly, clean up
|
|
1845
|
-
const failedServerInfo = this.activeServers.get(processId);
|
|
1846
|
-
if (failedServerInfo) {
|
|
1847
|
-
try {
|
|
1848
|
-
// Close browser if it exists
|
|
1849
|
-
if (failedServerInfo.browser) {
|
|
1850
|
-
await failedServerInfo.browser.close();
|
|
1851
|
-
}
|
|
1852
|
-
// Remove all event listeners to prevent memory leaks
|
|
1853
|
-
if (failedServerInfo.process) {
|
|
1854
|
-
failedServerInfo.process.stdout?.removeAllListeners('data');
|
|
1855
|
-
failedServerInfo.process.stderr?.removeAllListeners('data');
|
|
1856
|
-
failedServerInfo.process.removeAllListeners('exit');
|
|
1857
|
-
failedServerInfo.process.removeAllListeners('error');
|
|
1858
|
-
}
|
|
1859
|
-
// Kill the process
|
|
1860
|
-
if (failedServerInfo.pid) {
|
|
1861
|
-
process.kill(failedServerInfo.pid, 'SIGTERM');
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
catch (killError) {
|
|
1865
|
-
// Ignore errors during cleanup
|
|
1866
|
-
}
|
|
1867
|
-
this.activeServers.delete(processId);
|
|
1868
|
-
this.saveState();
|
|
1869
|
-
}
|
|
1870
|
-
throw error;
|
|
1871
|
-
}
|
|
1872
|
-
// Step 3: Wait a bit more for stability
|
|
1873
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ⏳ Waiting ${browserDelay}ms before launching browser...\n`);
|
|
1874
|
-
await new Promise(resolve => setTimeout(resolve, browserDelay));
|
|
1875
|
-
// Step 4: Launch the test browser
|
|
1876
|
-
try {
|
|
1877
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ✅ Launching test browser...\n`);
|
|
1878
|
-
const browser = await chromium.launch({
|
|
1879
|
-
headless: false,
|
|
1880
|
-
slowMo: teachingMode ? 50 : 0,
|
|
1881
|
-
devtools: teachingMode,
|
|
1882
|
-
args: ['--window-size=1920,1080', '--window-position=100,100']
|
|
1883
|
-
});
|
|
1884
|
-
const context = await browser.newContext({
|
|
1885
|
-
viewport: { width: 1920, height: 1080 }
|
|
1886
|
-
});
|
|
1887
|
-
const page = await context.newPage();
|
|
1888
|
-
// Add banner to identify test browser (hydration-safe version)
|
|
1889
|
-
// @ts-ignore
|
|
1890
|
-
await page.addInitScript(() => {
|
|
1891
|
-
function injectTestBanner() {
|
|
1892
|
-
// Create a container that won't interfere with React hydration
|
|
1893
|
-
const container = document.createElement('div');
|
|
1894
|
-
container.id = 'test-browser-banner-container';
|
|
1895
|
-
container.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; z-index: 2147483647; pointer-events: none;';
|
|
1896
|
-
// Use shadow DOM to isolate the banner completely
|
|
1897
|
-
const shadow = container.attachShadow({ mode: 'open' });
|
|
1898
|
-
// Create banner in shadow DOM
|
|
1899
|
-
const banner = document.createElement('div');
|
|
1900
|
-
banner.style.cssText = `
|
|
1901
|
-
position: fixed;
|
|
1902
|
-
top: 0;
|
|
1903
|
-
left: 0;
|
|
1904
|
-
right: 0;
|
|
1905
|
-
background: #22c55e;
|
|
1906
|
-
color: white;
|
|
1907
|
-
padding: 8px;
|
|
1908
|
-
text-align: center;
|
|
1909
|
-
font-family: monospace;
|
|
1910
|
-
font-size: 14px;
|
|
1911
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
1912
|
-
pointer-events: auto;
|
|
1913
|
-
`;
|
|
1914
|
-
banner.textContent = '🧪 TEST BROWSER - All console logs are being captured 📝';
|
|
1915
|
-
shadow.appendChild(banner);
|
|
1916
|
-
document.documentElement.appendChild(container);
|
|
1917
|
-
}
|
|
1918
|
-
// Wait for hydration to complete before injecting
|
|
1919
|
-
if (typeof window !== 'undefined') {
|
|
1920
|
-
// For Next.js apps, wait for hydration
|
|
1921
|
-
const waitForHydration = () => {
|
|
1922
|
-
let attempts = 0;
|
|
1923
|
-
const checkInterval = setInterval(() => {
|
|
1924
|
-
attempts++;
|
|
1925
|
-
// Check various hydration indicators
|
|
1926
|
-
const hydrated = window.__NEXT_HYDRATED ||
|
|
1927
|
-
window._react_root ||
|
|
1928
|
-
document.querySelector('[data-reactroot]') ||
|
|
1929
|
-
document.querySelector('#__next') ||
|
|
1930
|
-
attempts > 20; // Timeout after 5 seconds
|
|
1931
|
-
if (hydrated) {
|
|
1932
|
-
clearInterval(checkInterval);
|
|
1933
|
-
// Add a small delay to be extra safe
|
|
1934
|
-
setTimeout(injectTestBanner, 100);
|
|
1935
|
-
}
|
|
1936
|
-
}, 250);
|
|
1937
|
-
};
|
|
1938
|
-
// Start checking after DOMContentLoaded
|
|
1939
|
-
if (document.readyState === 'loading') {
|
|
1940
|
-
document.addEventListener('DOMContentLoaded', waitForHydration);
|
|
1941
|
-
}
|
|
1942
|
-
else {
|
|
1943
|
-
waitForHydration();
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
});
|
|
1947
|
-
// Set up console capture
|
|
1948
|
-
page.on('console', (msg) => {
|
|
1949
|
-
const timestamp = new Date().toISOString();
|
|
1950
|
-
const msgType = msg.type().toUpperCase();
|
|
1951
|
-
const msgText = msg.text();
|
|
1952
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [${msgType}] ${msgText}\n`;
|
|
1953
|
-
appendFileSync(outputFile, logEntry);
|
|
1954
|
-
// Flash red on errors
|
|
1955
|
-
if (msgType === 'ERROR') {
|
|
1956
|
-
page.evaluate(() => {
|
|
1957
|
-
// @ts-ignore - browser context
|
|
1958
|
-
const flash = document.createElement('div');
|
|
1959
|
-
flash.style.cssText = `
|
|
1960
|
-
position: fixed;
|
|
1961
|
-
top: 0;
|
|
1962
|
-
left: 0;
|
|
1963
|
-
right: 0;
|
|
1964
|
-
bottom: 0;
|
|
1965
|
-
background: rgba(239, 68, 68, 0.3);
|
|
1966
|
-
z-index: 999998;
|
|
1967
|
-
pointer-events: none;
|
|
1968
|
-
animation: errorFlash 0.5s ease-out;
|
|
1969
|
-
`;
|
|
1970
|
-
// @ts-ignore - browser context
|
|
1971
|
-
const style = document.createElement('style');
|
|
1972
|
-
style.textContent = `
|
|
1973
|
-
@keyframes errorFlash {
|
|
1974
|
-
0% { opacity: 1; }
|
|
1975
|
-
100% { opacity: 0; }
|
|
1976
|
-
}
|
|
1977
|
-
`;
|
|
1978
|
-
// @ts-ignore - browser context
|
|
1979
|
-
document.head.appendChild(style);
|
|
1980
|
-
// @ts-ignore - browser context
|
|
1981
|
-
document.body.appendChild(flash);
|
|
1982
|
-
setTimeout(() => flash.remove(), 500);
|
|
1983
|
-
}).catch(() => { }); // Ignore errors from flashing
|
|
1984
|
-
}
|
|
1985
|
-
});
|
|
1986
|
-
// Handle page errors (including hydration errors)
|
|
1987
|
-
page.on('pageerror', (error) => {
|
|
1988
|
-
const timestamp = new Date().toISOString();
|
|
1989
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [PAGE ERROR] ${error.message}\n${error.stack}\n`;
|
|
1990
|
-
appendFileSync(outputFile, logEntry);
|
|
1991
|
-
// Visual feedback for page errors
|
|
1992
|
-
page.evaluate(() => {
|
|
1993
|
-
// @ts-ignore - browser context
|
|
1994
|
-
const errorBanner = document.createElement('div');
|
|
1995
|
-
errorBanner.style.cssText = `
|
|
1996
|
-
position: fixed;
|
|
1997
|
-
top: 40px;
|
|
1998
|
-
left: 50%;
|
|
1999
|
-
transform: translateX(-50%);
|
|
2000
|
-
background: #dc2626;
|
|
2001
|
-
color: white;
|
|
2002
|
-
padding: 12px 24px;
|
|
2003
|
-
border-radius: 6px;
|
|
2004
|
-
font-family: monospace;
|
|
2005
|
-
font-size: 14px;
|
|
2006
|
-
z-index: 999999;
|
|
2007
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
|
|
2008
|
-
animation: slideDown 0.3s ease-out;
|
|
2009
|
-
`;
|
|
2010
|
-
errorBanner.textContent = '⚠️ JavaScript Error - Check Console & Logs';
|
|
2011
|
-
// @ts-ignore - browser context
|
|
2012
|
-
const style = document.createElement('style');
|
|
2013
|
-
style.textContent = `
|
|
2014
|
-
@keyframes slideDown {
|
|
2015
|
-
from { transform: translate(-50%, -100%); opacity: 0; }
|
|
2016
|
-
to { transform: translate(-50%, 0); opacity: 1; }
|
|
2017
|
-
}
|
|
2018
|
-
`;
|
|
2019
|
-
// @ts-ignore - browser context
|
|
2020
|
-
document.head.appendChild(style);
|
|
2021
|
-
// @ts-ignore - browser context
|
|
2022
|
-
document.body.appendChild(errorBanner);
|
|
2023
|
-
setTimeout(() => errorBanner.remove(), 5000);
|
|
2024
|
-
}).catch(() => { });
|
|
2025
|
-
});
|
|
2026
|
-
// Handle unhandled promise rejections
|
|
2027
|
-
await page.addInitScript(() => {
|
|
2028
|
-
// @ts-ignore - browser context
|
|
2029
|
-
window.addEventListener('unhandledrejection', (event) => {
|
|
2030
|
-
console.error('Unhandled Promise Rejection:', event.reason);
|
|
2031
|
-
});
|
|
2032
|
-
});
|
|
2033
|
-
// Handle network response errors
|
|
2034
|
-
page.on('response', (response) => {
|
|
2035
|
-
if (response.status() >= 400) {
|
|
2036
|
-
const timestamp = new Date().toISOString();
|
|
2037
|
-
const logEntry = `[${timestamp}] [${processId}] [BROWSER] [NETWORK ERROR] ${response.status()} ${response.statusText()} - ${response.url()}\n`;
|
|
2038
|
-
appendFileSync(outputFile, logEntry);
|
|
2039
|
-
}
|
|
2040
|
-
});
|
|
2041
|
-
// Navigate to the app
|
|
2042
|
-
const browserUrl = `http://localhost:${port}`;
|
|
2043
|
-
await page.goto(browserUrl, { waitUntil: 'domcontentloaded' });
|
|
2044
|
-
// Update server info with browser details
|
|
2045
|
-
serverInfo.browser = browser;
|
|
2046
|
-
serverInfo.page = page;
|
|
2047
|
-
serverInfo.browserUrl = browserUrl;
|
|
2048
|
-
serverInfo.consoleCapture = true;
|
|
2049
|
-
// Log success
|
|
2050
|
-
const successMessage = `
|
|
2051
|
-
🎯 TEST BROWSER LAUNCHED!
|
|
2052
|
-
|
|
2053
|
-
👉 Use the browser window that just opened (with green banner)
|
|
2054
|
-
📝 All console logs are being saved
|
|
2055
|
-
🔍 DevTools is open for debugging
|
|
2056
|
-
⚠️ Errors will flash red on screen
|
|
2057
|
-
|
|
2058
|
-
Happy debugging! 🚀
|
|
2059
|
-
`;
|
|
2060
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [BROWSER] Console capture started for ${browserUrl}\n`);
|
|
2061
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] ${successMessage}\n`);
|
|
2062
|
-
return {
|
|
2063
|
-
content: [
|
|
2064
|
-
{
|
|
2065
|
-
type: "text",
|
|
2066
|
-
text: JSON.stringify({
|
|
2067
|
-
status: "success",
|
|
2068
|
-
message: successMessage.trim(),
|
|
2069
|
-
processId: processId,
|
|
2070
|
-
serverPid: devProcess.pid,
|
|
2071
|
-
serverUrl: `http://localhost:${port}`,
|
|
2072
|
-
logFile: outputFile,
|
|
2073
|
-
browserStatus: "launched",
|
|
2074
|
-
teachingMode: teachingMode
|
|
2075
|
-
}, null, 2)
|
|
2076
|
-
}
|
|
2077
|
-
]
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
catch (browserError) {
|
|
2081
|
-
// Browser launch failed, but server is running
|
|
2082
|
-
const errorMsg = browserError instanceof Error ? browserError.message : String(browserError);
|
|
2083
|
-
appendFileSync(outputFile, `[${new Date().toISOString()}] [${processId}] [ERROR] Failed to launch browser: ${errorMsg}\n`);
|
|
2084
|
-
return {
|
|
2085
|
-
content: [
|
|
2086
|
-
{
|
|
2087
|
-
type: "text",
|
|
2088
|
-
text: JSON.stringify({
|
|
2089
|
-
status: "partial_success",
|
|
2090
|
-
message: `Server started successfully on http://localhost:${port}, but browser launch failed`,
|
|
2091
|
-
error: errorMsg,
|
|
2092
|
-
processId: processId,
|
|
2093
|
-
serverPid: devProcess.pid,
|
|
2094
|
-
serverUrl: `http://localhost:${port}`,
|
|
2095
|
-
logFile: outputFile,
|
|
2096
|
-
browserStatus: "failed",
|
|
2097
|
-
manualInstructions: "Open http://localhost:" + port + " in your browser manually"
|
|
2098
|
-
}, null, 2)
|
|
2099
|
-
}
|
|
2100
|
-
]
|
|
2101
|
-
};
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
catch (error) {
|
|
2105
|
-
throw new Error(`Failed to start frontend with browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
default:
|
|
2109
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
2110
|
-
}
|
|
270
|
+
case "dev_tail_sandbox_logs": {
|
|
271
|
+
const validatedArgs = TailSandboxLogsArgsSchema.parse(args);
|
|
272
|
+
const result = tailSandboxLogs(validatedArgs.lines);
|
|
273
|
+
return {
|
|
274
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
275
|
+
};
|
|
2111
276
|
}
|
|
2112
|
-
|
|
2113
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
277
|
+
default:
|
|
2114
278
|
return {
|
|
2115
|
-
content: [
|
|
2116
|
-
{
|
|
2117
|
-
type: "text",
|
|
2118
|
-
text: `Error: ${errorMessage}`
|
|
2119
|
-
}
|
|
2120
|
-
],
|
|
279
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
2121
280
|
isError: true
|
|
2122
281
|
};
|
|
2123
|
-
|
|
2124
|
-
});
|
|
282
|
+
}
|
|
2125
283
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
284
|
+
catch (error) {
|
|
285
|
+
return {
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
type: "text",
|
|
289
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
isError: true
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
// Cleanup on exit
|
|
297
|
+
process.on("SIGINT", () => {
|
|
298
|
+
if (sandboxProcess?.process) {
|
|
299
|
+
sandboxProcess.process.kill("SIGTERM");
|
|
300
|
+
}
|
|
301
|
+
process.exit(0);
|
|
302
|
+
});
|
|
303
|
+
process.on("SIGTERM", () => {
|
|
304
|
+
if (sandboxProcess?.process) {
|
|
305
|
+
sandboxProcess.process.kill("SIGTERM");
|
|
2130
306
|
}
|
|
307
|
+
process.exit(0);
|
|
308
|
+
});
|
|
309
|
+
// Start the server
|
|
310
|
+
async function main() {
|
|
311
|
+
const transport = new StdioServerTransport();
|
|
312
|
+
await server.connect(transport);
|
|
313
|
+
console.error("MCP Dev Logger server running on stdio");
|
|
2131
314
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
315
|
+
main().catch((error) => {
|
|
316
|
+
console.error("Fatal error in main():", error);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
2134
319
|
//# sourceMappingURL=index.js.map
|