dev3000 0.0.49 ā 0.0.51
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 +55 -6
- package/dist/cdp-monitor.d.ts.map +1 -1
- package/dist/cdp-monitor.js +54 -48
- package/dist/cdp-monitor.js.map +1 -1
- package/dist/cli.js +39 -33
- package/dist/cli.js.map +1 -1
- package/dist/dev-environment.d.ts +2 -0
- package/dist/dev-environment.d.ts.map +1 -1
- package/dist/dev-environment.js +212 -181
- package/dist/dev-environment.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/mcp-server/app/api/config/route.ts +7 -7
- package/mcp-server/app/api/logs/append/route.ts +59 -51
- package/mcp-server/app/api/logs/head/route.ts +22 -22
- package/mcp-server/app/api/logs/list/route.ts +39 -42
- package/mcp-server/app/api/logs/rotate/route.ts +28 -38
- package/mcp-server/app/api/logs/stream/route.ts +35 -35
- package/mcp-server/app/api/logs/tail/route.ts +22 -22
- package/mcp-server/app/api/mcp/[transport]/route.ts +189 -188
- package/mcp-server/app/api/replay/route.ts +217 -202
- package/mcp-server/app/layout.tsx +9 -8
- package/mcp-server/app/logs/LogsClient.test.ts +123 -99
- package/mcp-server/app/logs/LogsClient.tsx +724 -562
- package/mcp-server/app/logs/page.tsx +71 -72
- package/mcp-server/app/logs/utils.ts +99 -28
- package/mcp-server/app/page.tsx +10 -14
- package/mcp-server/app/replay/ReplayClient.tsx +120 -119
- package/mcp-server/app/replay/page.tsx +3 -3
- package/mcp-server/next.config.ts +2 -0
- package/mcp-server/package.json +5 -2
- package/mcp-server/pnpm-lock.yaml +37 -5
- package/mcp-server/tsconfig.json +4 -17
- package/package.json +16 -13
package/dist/dev-environment.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { tmpdir } from
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { CDPMonitor } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { appendFileSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "fs";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { tmpdir } from "os";
|
|
6
|
+
import { basename, dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { CDPMonitor } from "./cdp-monitor.js";
|
|
9
9
|
class Logger {
|
|
10
10
|
logFile;
|
|
11
11
|
constructor(logFile) {
|
|
@@ -16,7 +16,7 @@ class Logger {
|
|
|
16
16
|
mkdirSync(logDir, { recursive: true });
|
|
17
17
|
}
|
|
18
18
|
// Clear log file
|
|
19
|
-
writeFileSync(this.logFile,
|
|
19
|
+
writeFileSync(this.logFile, "");
|
|
20
20
|
}
|
|
21
21
|
log(source, message) {
|
|
22
22
|
const timestamp = new Date().toISOString();
|
|
@@ -25,25 +25,25 @@ class Logger {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
function detectPackageManagerForRun() {
|
|
28
|
-
if (existsSync(
|
|
29
|
-
return
|
|
30
|
-
if (existsSync(
|
|
31
|
-
return
|
|
32
|
-
if (existsSync(
|
|
33
|
-
return
|
|
34
|
-
return
|
|
28
|
+
if (existsSync("pnpm-lock.yaml"))
|
|
29
|
+
return "pnpm";
|
|
30
|
+
if (existsSync("yarn.lock"))
|
|
31
|
+
return "yarn";
|
|
32
|
+
if (existsSync("package-lock.json"))
|
|
33
|
+
return "npm";
|
|
34
|
+
return "npm"; // fallback
|
|
35
35
|
}
|
|
36
36
|
export function createPersistentLogFile() {
|
|
37
37
|
// Create /var/log/dev3000 directory
|
|
38
|
-
const logBaseDir =
|
|
38
|
+
const logBaseDir = "/var/log/dev3000";
|
|
39
39
|
try {
|
|
40
40
|
if (!existsSync(logBaseDir)) {
|
|
41
41
|
mkdirSync(logBaseDir, { recursive: true });
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
catch (
|
|
44
|
+
catch (_error) {
|
|
45
45
|
// Fallback to user's temp directory if /var/log is not writable
|
|
46
|
-
const fallbackDir = join(tmpdir(),
|
|
46
|
+
const fallbackDir = join(tmpdir(), "dev3000-logs");
|
|
47
47
|
if (!existsSync(fallbackDir)) {
|
|
48
48
|
mkdirSync(fallbackDir, { recursive: true });
|
|
49
49
|
}
|
|
@@ -53,26 +53,28 @@ export function createPersistentLogFile() {
|
|
|
53
53
|
}
|
|
54
54
|
function createLogFileInDir(baseDir) {
|
|
55
55
|
// Get current working directory name
|
|
56
|
-
const cwdName = basename(process.cwd()).replace(/[^a-zA-Z0-9-_]/g,
|
|
56
|
+
const cwdName = basename(process.cwd()).replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
57
57
|
// Create timestamp
|
|
58
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g,
|
|
58
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
59
59
|
// Create log file path
|
|
60
60
|
const logFileName = `dev3000-${cwdName}-${timestamp}.log`;
|
|
61
61
|
const logFilePath = join(baseDir, logFileName);
|
|
62
62
|
// Prune old logs for this project (keep only 10 most recent)
|
|
63
63
|
pruneOldLogs(baseDir, cwdName);
|
|
64
64
|
// Create the log file
|
|
65
|
-
writeFileSync(logFilePath,
|
|
66
|
-
// Create or update
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
writeFileSync(logFilePath, "");
|
|
66
|
+
// Create or update symlinks to /tmp/dev3000.log and /tmp/d3k.log
|
|
67
|
+
const symlinkPaths = ["/tmp/dev3000.log", "/tmp/d3k.log"];
|
|
68
|
+
for (const symlinkPath of symlinkPaths) {
|
|
69
|
+
try {
|
|
70
|
+
if (existsSync(symlinkPath)) {
|
|
71
|
+
unlinkSync(symlinkPath);
|
|
72
|
+
}
|
|
73
|
+
symlinkSync(logFilePath, symlinkPath);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn(chalk.yellow(`ā ļø Could not create symlink ${symlinkPath}: ${error}`));
|
|
71
77
|
}
|
|
72
|
-
symlinkSync(logFilePath, symlinkPath);
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.warn(chalk.yellow(`ā ļø Could not create symlink ${symlinkPath}: ${error}`));
|
|
76
78
|
}
|
|
77
79
|
return logFilePath;
|
|
78
80
|
}
|
|
@@ -80,11 +82,11 @@ function pruneOldLogs(baseDir, cwdName) {
|
|
|
80
82
|
try {
|
|
81
83
|
// Find all log files for this project
|
|
82
84
|
const files = readdirSync(baseDir)
|
|
83
|
-
.filter(file => file.startsWith(`dev3000-${cwdName}-`) && file.endsWith(
|
|
84
|
-
.map(file => ({
|
|
85
|
+
.filter((file) => file.startsWith(`dev3000-${cwdName}-`) && file.endsWith(".log"))
|
|
86
|
+
.map((file) => ({
|
|
85
87
|
name: file,
|
|
86
88
|
path: join(baseDir, file),
|
|
87
|
-
mtime: statSync(join(baseDir, file)).mtime
|
|
89
|
+
mtime: statSync(join(baseDir, file)).mtime,
|
|
88
90
|
}))
|
|
89
91
|
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); // Most recent first
|
|
90
92
|
// Keep only the 10 most recent, delete the rest
|
|
@@ -94,7 +96,7 @@ function pruneOldLogs(baseDir, cwdName) {
|
|
|
94
96
|
try {
|
|
95
97
|
unlinkSync(file.path);
|
|
96
98
|
}
|
|
97
|
-
catch (
|
|
99
|
+
catch (_error) {
|
|
98
100
|
// Silently ignore deletion errors
|
|
99
101
|
}
|
|
100
102
|
}
|
|
@@ -124,35 +126,36 @@ export class DevEnvironment {
|
|
|
124
126
|
const packageRoot = dirname(dirname(currentFile));
|
|
125
127
|
// Always use MCP server's public directory for screenshots to ensure they're web-accessible
|
|
126
128
|
// and avoid permission issues with /var/log paths
|
|
127
|
-
this.screenshotDir = join(packageRoot,
|
|
128
|
-
this.pidFile = join(tmpdir(),
|
|
129
|
-
this.mcpPublicDir = join(packageRoot,
|
|
129
|
+
this.screenshotDir = join(packageRoot, "mcp-server", "public", "screenshots");
|
|
130
|
+
this.pidFile = join(tmpdir(), "dev3000.pid");
|
|
131
|
+
this.mcpPublicDir = join(packageRoot, "mcp-server", "public", "screenshots");
|
|
130
132
|
// Read version from package.json for startup message
|
|
131
|
-
this.version =
|
|
133
|
+
this.version = "0.0.0";
|
|
132
134
|
try {
|
|
133
|
-
const packageJsonPath = join(packageRoot,
|
|
134
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath,
|
|
135
|
+
const packageJsonPath = join(packageRoot, "package.json");
|
|
136
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
135
137
|
this.version = packageJson.version;
|
|
136
138
|
// Use git to detect if we're in the dev3000 source repository
|
|
137
139
|
try {
|
|
138
|
-
const { execSync } = require(
|
|
139
|
-
const gitRemote = execSync(
|
|
140
|
+
const { execSync } = require("child_process");
|
|
141
|
+
const gitRemote = execSync("git remote get-url origin 2>/dev/null", {
|
|
140
142
|
cwd: packageRoot,
|
|
141
|
-
encoding:
|
|
143
|
+
encoding: "utf8",
|
|
142
144
|
}).trim();
|
|
143
|
-
if (gitRemote.includes(
|
|
144
|
-
this.version
|
|
145
|
+
if (gitRemote.includes("vercel-labs/dev3000") &&
|
|
146
|
+
!this.version.includes("canary")) {
|
|
147
|
+
this.version += "-local";
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
catch {
|
|
148
151
|
// Not in git repo or no git - use version as-is
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
|
-
catch (
|
|
154
|
+
catch (_error) {
|
|
152
155
|
// Use fallback version
|
|
153
156
|
}
|
|
154
157
|
// Initialize spinner for clean output management
|
|
155
|
-
this.spinner = ora({ text:
|
|
158
|
+
this.spinner = ora({ text: "Initializing...", spinner: "dots" });
|
|
156
159
|
// Ensure directories exist
|
|
157
160
|
if (!existsSync(this.screenshotDir)) {
|
|
158
161
|
mkdirSync(this.screenshotDir, { recursive: true });
|
|
@@ -166,15 +169,16 @@ export class DevEnvironment {
|
|
|
166
169
|
for (const port of ports) {
|
|
167
170
|
try {
|
|
168
171
|
const result = await new Promise((resolve) => {
|
|
169
|
-
const proc = spawn(
|
|
170
|
-
let output =
|
|
171
|
-
|
|
172
|
-
proc.on(
|
|
172
|
+
const proc = spawn("lsof", ["-ti", `:${port}`], { stdio: "pipe" });
|
|
173
|
+
let output = "";
|
|
174
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: whatever
|
|
175
|
+
proc.stdout?.on("data", (data) => (output += data.toString()));
|
|
176
|
+
proc.on("exit", () => resolve(output.trim()));
|
|
173
177
|
});
|
|
174
178
|
if (result) {
|
|
175
|
-
result.split(
|
|
179
|
+
result.split("\n").filter((line) => line.trim());
|
|
176
180
|
// Stop spinner and show error
|
|
177
|
-
if (this.spinner
|
|
181
|
+
if (this.spinner?.isSpinning) {
|
|
178
182
|
this.spinner.fail(`Port ${port} is already in use`);
|
|
179
183
|
}
|
|
180
184
|
console.log(chalk.yellow(`š” To free up port ${port}, run: lsof -ti:${port} | xargs kill -9`));
|
|
@@ -182,7 +186,7 @@ export class DevEnvironment {
|
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
catch (error) {
|
|
185
|
-
if (error instanceof Error && error.message.includes(
|
|
189
|
+
if (error instanceof Error && error.message.includes("Port")) {
|
|
186
190
|
throw error; // Re-throw our custom error
|
|
187
191
|
}
|
|
188
192
|
// Ignore other errors - port might just be free
|
|
@@ -191,77 +195,86 @@ export class DevEnvironment {
|
|
|
191
195
|
}
|
|
192
196
|
async start() {
|
|
193
197
|
// Show startup message first
|
|
194
|
-
console.log(chalk.
|
|
198
|
+
console.log(chalk.greenBright(`Starting ${this.options.commandName} (v${this.version})`));
|
|
195
199
|
// Start spinner
|
|
196
|
-
this.spinner.start(
|
|
200
|
+
this.spinner.start("Checking ports...");
|
|
197
201
|
// Check if ports are available first
|
|
198
202
|
await this.checkPortsAvailable();
|
|
199
|
-
this.spinner.text =
|
|
203
|
+
this.spinner.text = "Setting up environment...";
|
|
200
204
|
// Write our process group ID to PID file for cleanup
|
|
201
205
|
writeFileSync(this.pidFile, process.pid.toString());
|
|
202
206
|
// Setup cleanup handlers
|
|
203
207
|
this.setupCleanupHandlers();
|
|
204
208
|
// Start user's dev server
|
|
205
|
-
this.spinner.text =
|
|
209
|
+
this.spinner.text = "Starting your dev server...";
|
|
206
210
|
await this.startServer();
|
|
207
211
|
// Start MCP server
|
|
208
|
-
this.spinner.text =
|
|
212
|
+
this.spinner.text = `Starting ${this.options.commandName} services...`;
|
|
209
213
|
await this.startMcpServer();
|
|
210
214
|
// Wait for servers to be ready
|
|
211
|
-
this.spinner.text =
|
|
215
|
+
this.spinner.text = "Waiting for your app server...";
|
|
212
216
|
await this.waitForServer();
|
|
213
|
-
this.spinner.text =
|
|
217
|
+
this.spinner.text = `Waiting for ${this.options.commandName} services...`;
|
|
214
218
|
await this.waitForMcpServer();
|
|
215
|
-
// Start CDP monitoring
|
|
216
|
-
this.
|
|
217
|
-
|
|
219
|
+
// Start CDP monitoring if not in servers-only mode
|
|
220
|
+
if (!this.options.serversOnly) {
|
|
221
|
+
this.spinner.text = "Launching browser monitor...";
|
|
222
|
+
this.startCDPMonitoringAsync();
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
this.debugLog("Browser monitoring disabled via --servers-only flag");
|
|
226
|
+
}
|
|
218
227
|
// Complete startup
|
|
219
|
-
this.spinner.succeed(
|
|
220
|
-
console.log(chalk.
|
|
221
|
-
console.log(chalk.
|
|
222
|
-
console.log(chalk.
|
|
223
|
-
console.log(chalk.
|
|
224
|
-
console.log(chalk.
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
this.spinner.succeed("Development environment ready!");
|
|
229
|
+
console.log(chalk.cyan(`Logs: /tmp/d3k.log -> ${this.options.logFile}`));
|
|
230
|
+
console.log(chalk.cyan("āļø Give this to an AI to auto debug and fix your app\n"));
|
|
231
|
+
console.log(chalk.cyan(`š Your App: http://localhost:${this.options.port}`));
|
|
232
|
+
console.log(chalk.cyan(`š¤ MCP Server: http://localhost:${this.options.mcpPort}/api/mcp/mcp`));
|
|
233
|
+
console.log(chalk.cyan(`šø Visual Timeline: http://localhost:${this.options.mcpPort}/logs`));
|
|
234
|
+
if (this.options.serversOnly) {
|
|
235
|
+
console.log(chalk.cyan("š„ļø Servers-only mode - use Chrome extension for browser monitoring"));
|
|
236
|
+
}
|
|
237
|
+
console.log(chalk.gray(`\nš” To stop all servers and kill ${this.options.commandName}: Ctrl-C`));
|
|
227
238
|
}
|
|
228
239
|
async startServer() {
|
|
229
|
-
const [command, ...args] = this.options.serverCommand.split(
|
|
240
|
+
const [command, ...args] = this.options.serverCommand.split(" ");
|
|
230
241
|
this.serverProcess = spawn(command, args, {
|
|
231
|
-
stdio: [
|
|
242
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
232
243
|
shell: true,
|
|
233
244
|
detached: true, // Run independently
|
|
234
245
|
});
|
|
235
246
|
// Log server output (to file only, reduce stdout noise)
|
|
236
|
-
this.serverProcess.stdout?.on(
|
|
247
|
+
this.serverProcess.stdout?.on("data", (data) => {
|
|
237
248
|
const message = data.toString().trim();
|
|
238
249
|
if (message) {
|
|
239
|
-
this.logger.log(
|
|
250
|
+
this.logger.log("server", message);
|
|
240
251
|
}
|
|
241
252
|
});
|
|
242
|
-
this.serverProcess.stderr?.on(
|
|
253
|
+
this.serverProcess.stderr?.on("data", (data) => {
|
|
243
254
|
const message = data.toString().trim();
|
|
244
255
|
if (message) {
|
|
245
|
-
this.logger.log(
|
|
256
|
+
this.logger.log("server", `ERROR: ${message}`);
|
|
246
257
|
// Suppress build errors and common dev errors from console output
|
|
247
258
|
// They're still logged to file for debugging
|
|
248
259
|
// Only show truly critical errors that would prevent startup
|
|
249
|
-
const isCriticalError = message.includes(
|
|
250
|
-
message.includes(
|
|
251
|
-
message.includes(
|
|
252
|
-
(message.includes(
|
|
253
|
-
|
|
260
|
+
const isCriticalError = message.includes("EADDRINUSE") ||
|
|
261
|
+
message.includes("EACCES") ||
|
|
262
|
+
message.includes("ENOENT") ||
|
|
263
|
+
(message.includes("FATAL") &&
|
|
264
|
+
!message.includes("generateStaticParams")) ||
|
|
265
|
+
(message.includes("Cannot find module") &&
|
|
266
|
+
!message.includes(".next"));
|
|
254
267
|
if (isCriticalError) {
|
|
255
|
-
console.error(chalk.red(
|
|
268
|
+
console.error(chalk.red("[CRITICAL ERROR]"), message);
|
|
256
269
|
}
|
|
257
270
|
}
|
|
258
271
|
});
|
|
259
|
-
this.serverProcess.on(
|
|
272
|
+
this.serverProcess.on("exit", (code) => {
|
|
260
273
|
if (this.isShuttingDown)
|
|
261
274
|
return; // Don't handle exits during shutdown
|
|
262
275
|
if (code !== 0 && code !== null) {
|
|
263
276
|
this.debugLog(`Server process exited with code ${code}`);
|
|
264
|
-
this.logger.log(
|
|
277
|
+
this.logger.log("server", `Server process exited with code ${code}`);
|
|
265
278
|
// Only shutdown for truly fatal exit codes, not build failures or restarts
|
|
266
279
|
// Common exit codes that indicate temporary issues, not fatal errors:
|
|
267
280
|
// - Code 1: Generic build failure or restart
|
|
@@ -270,19 +283,19 @@ export class DevEnvironment {
|
|
|
270
283
|
const isFatalExit = code !== 1 && code !== 130 && code !== 143;
|
|
271
284
|
if (isFatalExit) {
|
|
272
285
|
// Stop spinner and show error for fatal exits only
|
|
273
|
-
if (this.spinner
|
|
286
|
+
if (this.spinner?.isSpinning) {
|
|
274
287
|
this.spinner.fail(`Server process fatally exited with code ${code}`);
|
|
275
288
|
}
|
|
276
289
|
else {
|
|
277
290
|
console.log(chalk.red(`\nā Server process fatally exited with code ${code}`));
|
|
278
291
|
}
|
|
279
|
-
console.log(chalk.yellow(
|
|
292
|
+
console.log(chalk.yellow("š” Check your server command and logs for details"));
|
|
280
293
|
this.gracefulShutdown();
|
|
281
294
|
}
|
|
282
295
|
else {
|
|
283
296
|
// For non-fatal exits (like build failures), just log and continue
|
|
284
|
-
if (this.spinner
|
|
285
|
-
this.spinner.text =
|
|
297
|
+
if (this.spinner?.isSpinning) {
|
|
298
|
+
this.spinner.text = "Server process restarted, waiting...";
|
|
286
299
|
}
|
|
287
300
|
}
|
|
288
301
|
}
|
|
@@ -290,7 +303,7 @@ export class DevEnvironment {
|
|
|
290
303
|
}
|
|
291
304
|
debugLog(message) {
|
|
292
305
|
if (this.options.debug) {
|
|
293
|
-
if (this.spinner
|
|
306
|
+
if (this.spinner?.isSpinning) {
|
|
294
307
|
// Temporarily stop the spinner, show debug message, then restart
|
|
295
308
|
const currentText = this.spinner.text;
|
|
296
309
|
this.spinner.stop();
|
|
@@ -303,40 +316,46 @@ export class DevEnvironment {
|
|
|
303
316
|
}
|
|
304
317
|
}
|
|
305
318
|
async startMcpServer() {
|
|
306
|
-
this.debugLog(
|
|
319
|
+
this.debugLog("Starting MCP server setup");
|
|
307
320
|
// Get the path to our bundled MCP server
|
|
308
321
|
const currentFile = fileURLToPath(import.meta.url);
|
|
309
322
|
const packageRoot = dirname(dirname(currentFile)); // Go up from dist/ to package root
|
|
310
|
-
const mcpServerPath = join(packageRoot,
|
|
323
|
+
const mcpServerPath = join(packageRoot, "mcp-server");
|
|
311
324
|
this.debugLog(`MCP server path: ${mcpServerPath}`);
|
|
312
325
|
if (!existsSync(mcpServerPath)) {
|
|
313
326
|
throw new Error(`MCP server directory not found at ${mcpServerPath}`);
|
|
314
327
|
}
|
|
315
|
-
this.debugLog(
|
|
328
|
+
this.debugLog("MCP server directory found");
|
|
316
329
|
// Check if MCP server dependencies are installed, install if missing
|
|
317
|
-
const isGlobalInstall = mcpServerPath.includes(
|
|
330
|
+
const isGlobalInstall = mcpServerPath.includes(".pnpm");
|
|
318
331
|
this.debugLog(`Is global install: ${isGlobalInstall}`);
|
|
319
|
-
let nodeModulesPath = join(mcpServerPath,
|
|
332
|
+
let nodeModulesPath = join(mcpServerPath, "node_modules");
|
|
320
333
|
let actualWorkingDir = mcpServerPath;
|
|
321
334
|
this.debugLog(`Node modules path: ${nodeModulesPath}`);
|
|
322
335
|
if (isGlobalInstall) {
|
|
323
|
-
const tmpDirPath = join(tmpdir(),
|
|
324
|
-
nodeModulesPath = join(tmpDirPath,
|
|
336
|
+
const tmpDirPath = join(tmpdir(), "dev3000-mcp-deps");
|
|
337
|
+
nodeModulesPath = join(tmpDirPath, "node_modules");
|
|
325
338
|
actualWorkingDir = tmpDirPath;
|
|
326
339
|
// Update screenshot and MCP public directory to use the temp directory for global installs
|
|
327
|
-
this.screenshotDir = join(actualWorkingDir,
|
|
328
|
-
this.mcpPublicDir = join(actualWorkingDir,
|
|
340
|
+
this.screenshotDir = join(actualWorkingDir, "public", "screenshots");
|
|
341
|
+
this.mcpPublicDir = join(actualWorkingDir, "public", "screenshots");
|
|
329
342
|
if (!existsSync(this.mcpPublicDir)) {
|
|
330
343
|
mkdirSync(this.mcpPublicDir, { recursive: true });
|
|
331
344
|
}
|
|
332
345
|
}
|
|
333
346
|
// Always install dependencies to ensure they're up to date
|
|
334
|
-
this.debugLog(
|
|
347
|
+
this.debugLog("Installing/updating MCP server dependencies");
|
|
335
348
|
await this.installMcpServerDeps(mcpServerPath);
|
|
336
349
|
// Use version already read in constructor
|
|
337
350
|
// For global installs, ensure all necessary files are copied to temp directory
|
|
338
351
|
if (isGlobalInstall && actualWorkingDir !== mcpServerPath) {
|
|
339
|
-
const requiredFiles = [
|
|
352
|
+
const requiredFiles = [
|
|
353
|
+
"app",
|
|
354
|
+
"public",
|
|
355
|
+
"next.config.ts",
|
|
356
|
+
"next-env.d.ts",
|
|
357
|
+
"tsconfig.json",
|
|
358
|
+
];
|
|
340
359
|
for (const file of requiredFiles) {
|
|
341
360
|
const srcPath = join(mcpServerPath, file);
|
|
342
361
|
const destPath = join(actualWorkingDir, file);
|
|
@@ -353,7 +372,7 @@ export class DevEnvironment {
|
|
|
353
372
|
// Remove existing destination if it exists
|
|
354
373
|
if (existsSync(destPath)) {
|
|
355
374
|
if (lstatSync(destPath).isDirectory()) {
|
|
356
|
-
cpSync(destPath, destPath
|
|
375
|
+
cpSync(destPath, `${destPath}.bak`, { recursive: true });
|
|
357
376
|
cpSync(srcPath, destPath, { recursive: true, force: true });
|
|
358
377
|
}
|
|
359
378
|
else {
|
|
@@ -378,8 +397,8 @@ export class DevEnvironment {
|
|
|
378
397
|
this.debugLog(`Using package manager: ${packageManagerForRun}`);
|
|
379
398
|
this.debugLog(`MCP server working directory: ${actualWorkingDir}`);
|
|
380
399
|
this.debugLog(`MCP server port: ${this.options.mcpPort}`);
|
|
381
|
-
this.mcpServerProcess = spawn(packageManagerForRun, [
|
|
382
|
-
stdio: [
|
|
400
|
+
this.mcpServerProcess = spawn(packageManagerForRun, ["run", "dev"], {
|
|
401
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
383
402
|
shell: true,
|
|
384
403
|
detached: true, // Run independently
|
|
385
404
|
cwd: actualWorkingDir,
|
|
@@ -390,36 +409,36 @@ export class DevEnvironment {
|
|
|
390
409
|
DEV3000_VERSION: this.version, // Pass version to MCP server
|
|
391
410
|
},
|
|
392
411
|
});
|
|
393
|
-
this.debugLog(
|
|
412
|
+
this.debugLog("MCP server process spawned");
|
|
394
413
|
// Log MCP server output to separate file for debugging
|
|
395
|
-
const mcpLogFile = join(dirname(this.options.logFile),
|
|
396
|
-
writeFileSync(mcpLogFile,
|
|
397
|
-
this.mcpServerProcess.stdout?.on(
|
|
414
|
+
const mcpLogFile = join(dirname(this.options.logFile), "dev3000-mcp.log");
|
|
415
|
+
writeFileSync(mcpLogFile, ""); // Clear the file
|
|
416
|
+
this.mcpServerProcess.stdout?.on("data", (data) => {
|
|
398
417
|
const message = data.toString().trim();
|
|
399
418
|
if (message) {
|
|
400
419
|
const timestamp = new Date().toISOString();
|
|
401
420
|
appendFileSync(mcpLogFile, `[${timestamp}] [MCP-STDOUT] ${message}\n`);
|
|
402
421
|
}
|
|
403
422
|
});
|
|
404
|
-
this.mcpServerProcess.stderr?.on(
|
|
423
|
+
this.mcpServerProcess.stderr?.on("data", (data) => {
|
|
405
424
|
const message = data.toString().trim();
|
|
406
425
|
if (message) {
|
|
407
426
|
const timestamp = new Date().toISOString();
|
|
408
427
|
appendFileSync(mcpLogFile, `[${timestamp}] [MCP-STDERR] ${message}\n`);
|
|
409
428
|
// Only show critical errors in stdout for debugging
|
|
410
|
-
if (message.includes(
|
|
411
|
-
console.error(chalk.red(
|
|
429
|
+
if (message.includes("FATAL") || message.includes("Error:")) {
|
|
430
|
+
console.error(chalk.red("[LOG VIEWER ERROR]"), message);
|
|
412
431
|
}
|
|
413
432
|
}
|
|
414
433
|
});
|
|
415
|
-
this.mcpServerProcess.on(
|
|
434
|
+
this.mcpServerProcess.on("exit", (code) => {
|
|
416
435
|
this.debugLog(`MCP server process exited with code ${code}`);
|
|
417
436
|
// Only show exit messages for unexpected failures, not restarts
|
|
418
437
|
if (code !== 0 && code !== null) {
|
|
419
|
-
this.logger.log(
|
|
438
|
+
this.logger.log("server", `MCP server process exited with code ${code}`);
|
|
420
439
|
}
|
|
421
440
|
});
|
|
422
|
-
this.debugLog(
|
|
441
|
+
this.debugLog("MCP server event handlers setup complete");
|
|
423
442
|
}
|
|
424
443
|
async waitForServer() {
|
|
425
444
|
const maxAttempts = 30;
|
|
@@ -427,18 +446,18 @@ export class DevEnvironment {
|
|
|
427
446
|
while (attempts < maxAttempts) {
|
|
428
447
|
try {
|
|
429
448
|
const response = await fetch(`http://localhost:${this.options.port}`, {
|
|
430
|
-
method:
|
|
431
|
-
signal: AbortSignal.timeout(2000)
|
|
449
|
+
method: "HEAD",
|
|
450
|
+
signal: AbortSignal.timeout(2000),
|
|
432
451
|
});
|
|
433
452
|
if (response.ok || response.status === 404) {
|
|
434
453
|
return;
|
|
435
454
|
}
|
|
436
455
|
}
|
|
437
|
-
catch (
|
|
456
|
+
catch (_error) {
|
|
438
457
|
// Server not ready yet, continue waiting
|
|
439
458
|
}
|
|
440
459
|
attempts++;
|
|
441
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
460
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
442
461
|
}
|
|
443
462
|
// Continue anyway if health check fails
|
|
444
463
|
}
|
|
@@ -446,44 +465,46 @@ export class DevEnvironment {
|
|
|
446
465
|
return new Promise((resolve, reject) => {
|
|
447
466
|
// For global installs, we need to install to a writable location
|
|
448
467
|
// Check if this is a global install by looking for .pnpm in the path
|
|
449
|
-
const isGlobalInstall = mcpServerPath.includes(
|
|
468
|
+
const isGlobalInstall = mcpServerPath.includes(".pnpm");
|
|
450
469
|
let workingDir = mcpServerPath;
|
|
451
470
|
if (isGlobalInstall) {
|
|
452
471
|
// Create a writable copy in temp directory for global installs
|
|
453
|
-
const tmpDirPath = join(tmpdir(),
|
|
472
|
+
const tmpDirPath = join(tmpdir(), "dev3000-mcp-deps");
|
|
454
473
|
// Ensure tmp directory exists
|
|
455
474
|
if (!existsSync(tmpDirPath)) {
|
|
456
475
|
mkdirSync(tmpDirPath, { recursive: true });
|
|
457
476
|
}
|
|
458
|
-
//
|
|
459
|
-
const tmpPackageJson = join(tmpDirPath,
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
copyFileSync(sourcePackageJson, tmpPackageJson);
|
|
463
|
-
}
|
|
477
|
+
// Always copy package.json to temp directory to ensure it's up to date
|
|
478
|
+
const tmpPackageJson = join(tmpDirPath, "package.json");
|
|
479
|
+
const sourcePackageJson = join(mcpServerPath, "package.json");
|
|
480
|
+
copyFileSync(sourcePackageJson, tmpPackageJson);
|
|
464
481
|
workingDir = tmpDirPath;
|
|
465
482
|
}
|
|
466
483
|
const packageManager = detectPackageManagerForRun();
|
|
467
484
|
// Don't show any console output during dependency installation
|
|
468
485
|
// All status will be handled by the progress bar
|
|
469
|
-
|
|
470
|
-
|
|
486
|
+
// For pnpm, use --dev flag to include devDependencies
|
|
487
|
+
const installArgs = packageManager === "pnpm"
|
|
488
|
+
? ["install", "--prod=false"] // Install both prod and dev dependencies
|
|
489
|
+
: ["install", "--include=dev"]; // npm/yarn syntax
|
|
490
|
+
const installProcess = spawn(packageManager, installArgs, {
|
|
491
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
471
492
|
shell: true,
|
|
472
493
|
cwd: workingDir,
|
|
473
494
|
});
|
|
474
495
|
// Add timeout (3 minutes)
|
|
475
496
|
const timeout = setTimeout(() => {
|
|
476
|
-
installProcess.kill(
|
|
477
|
-
reject(new Error(
|
|
497
|
+
installProcess.kill("SIGKILL");
|
|
498
|
+
reject(new Error("MCP server dependency installation timed out after 3 minutes"));
|
|
478
499
|
}, 3 * 60 * 1000);
|
|
479
500
|
// Suppress all output to prevent progress bar interference
|
|
480
|
-
installProcess.stdout?.on(
|
|
501
|
+
installProcess.stdout?.on("data", (_data) => {
|
|
481
502
|
// Silently consume output
|
|
482
503
|
});
|
|
483
|
-
installProcess.stderr?.on(
|
|
504
|
+
installProcess.stderr?.on("data", (_data) => {
|
|
484
505
|
// Silently consume output
|
|
485
506
|
});
|
|
486
|
-
installProcess.on(
|
|
507
|
+
installProcess.on("exit", (code) => {
|
|
487
508
|
clearTimeout(timeout);
|
|
488
509
|
if (code === 0) {
|
|
489
510
|
resolve();
|
|
@@ -492,7 +513,7 @@ export class DevEnvironment {
|
|
|
492
513
|
reject(new Error(`MCP server dependency installation failed with exit code ${code}`));
|
|
493
514
|
}
|
|
494
515
|
});
|
|
495
|
-
installProcess.on(
|
|
516
|
+
installProcess.on("error", (error) => {
|
|
496
517
|
clearTimeout(timeout);
|
|
497
518
|
reject(new Error(`Failed to start MCP server dependency installation: ${error.message}`));
|
|
498
519
|
});
|
|
@@ -505,15 +526,16 @@ export class DevEnvironment {
|
|
|
505
526
|
try {
|
|
506
527
|
// Test the actual MCP endpoint
|
|
507
528
|
const response = await fetch(`http://localhost:${this.options.mcpPort}`, {
|
|
508
|
-
method:
|
|
509
|
-
signal: AbortSignal.timeout(2000)
|
|
529
|
+
method: "HEAD",
|
|
530
|
+
signal: AbortSignal.timeout(2000),
|
|
510
531
|
});
|
|
511
532
|
this.debugLog(`MCP server health check: ${response.status}`);
|
|
512
533
|
if (response.status === 500) {
|
|
513
534
|
const errorText = await response.text();
|
|
514
535
|
this.debugLog(`MCP server 500 error: ${errorText}`);
|
|
515
536
|
}
|
|
516
|
-
if (response.ok || response.status === 404) {
|
|
537
|
+
if (response.ok || response.status === 404) {
|
|
538
|
+
// 404 is OK - means server is responding
|
|
517
539
|
return;
|
|
518
540
|
}
|
|
519
541
|
}
|
|
@@ -521,39 +543,48 @@ export class DevEnvironment {
|
|
|
521
543
|
this.debugLog(`MCP server not ready (attempt ${attempts}): ${error}`);
|
|
522
544
|
}
|
|
523
545
|
attempts++;
|
|
524
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
546
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
525
547
|
}
|
|
526
|
-
this.debugLog(
|
|
548
|
+
this.debugLog("MCP server health check failed, terminating");
|
|
527
549
|
throw new Error(`MCP server failed to start after ${maxAttempts} seconds. Check the logs for errors.`);
|
|
528
550
|
}
|
|
529
551
|
startCDPMonitoringAsync() {
|
|
552
|
+
// Skip if in servers-only mode
|
|
553
|
+
if (this.options.serversOnly) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
530
556
|
// Start CDP monitoring in background without blocking completion
|
|
531
|
-
this.startCDPMonitoring().catch(error => {
|
|
532
|
-
console.error(chalk.red(
|
|
557
|
+
this.startCDPMonitoring().catch((error) => {
|
|
558
|
+
console.error(chalk.red("ā ļø CDP monitoring setup failed:"), error);
|
|
533
559
|
// CDP monitoring is critical - shutdown if it fails
|
|
534
560
|
this.gracefulShutdown();
|
|
535
561
|
});
|
|
536
562
|
}
|
|
537
563
|
async startCDPMonitoring() {
|
|
564
|
+
// Skip if in servers-only mode
|
|
565
|
+
if (this.options.serversOnly) {
|
|
566
|
+
this.debugLog("Browser monitoring disabled via --servers-only flag");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
538
569
|
// Ensure profile directory exists
|
|
539
570
|
if (!existsSync(this.options.profileDir)) {
|
|
540
571
|
mkdirSync(this.options.profileDir, { recursive: true });
|
|
541
572
|
}
|
|
542
573
|
// Initialize CDP monitor with enhanced logging - use MCP public directory for screenshots
|
|
543
|
-
this.cdpMonitor = new CDPMonitor(this.options.profileDir, this.mcpPublicDir, (
|
|
544
|
-
this.logger.log(
|
|
574
|
+
this.cdpMonitor = new CDPMonitor(this.options.profileDir, this.mcpPublicDir, (_source, message) => {
|
|
575
|
+
this.logger.log("browser", message);
|
|
545
576
|
}, this.options.debug);
|
|
546
577
|
try {
|
|
547
578
|
// Start CDP monitoring
|
|
548
579
|
await this.cdpMonitor.start();
|
|
549
|
-
this.logger.log(
|
|
580
|
+
this.logger.log("browser", "[CDP] Chrome launched with DevTools Protocol monitoring");
|
|
550
581
|
// Navigate to the app
|
|
551
582
|
await this.cdpMonitor.navigateToApp(this.options.port);
|
|
552
|
-
this.logger.log(
|
|
583
|
+
this.logger.log("browser", `[CDP] Navigated to http://localhost:${this.options.port}`);
|
|
553
584
|
}
|
|
554
585
|
catch (error) {
|
|
555
586
|
// Log error and throw to trigger graceful shutdown
|
|
556
|
-
this.logger.log(
|
|
587
|
+
this.logger.log("browser", `[CDP ERROR] Failed to start CDP monitoring: ${error}`);
|
|
557
588
|
throw error;
|
|
558
589
|
}
|
|
559
590
|
}
|
|
@@ -562,17 +593,17 @@ export class DevEnvironment {
|
|
|
562
593
|
return; // Prevent multiple shutdown attempts
|
|
563
594
|
this.isShuttingDown = true;
|
|
564
595
|
// Stop spinner if it's running
|
|
565
|
-
if (this.spinner
|
|
566
|
-
this.spinner.fail(
|
|
596
|
+
if (this.spinner?.isSpinning) {
|
|
597
|
+
this.spinner.fail("Critical failure detected");
|
|
567
598
|
}
|
|
568
|
-
console.log(chalk.yellow(
|
|
599
|
+
console.log(chalk.yellow(`š Shutting down ${this.options.commandName} due to critical failure...`));
|
|
569
600
|
// Kill processes on both ports
|
|
570
601
|
const killPortProcess = async (port, name) => {
|
|
571
602
|
try {
|
|
572
|
-
const { spawn } = await import(
|
|
573
|
-
const killProcess = spawn(
|
|
603
|
+
const { spawn } = await import("child_process");
|
|
604
|
+
const killProcess = spawn("sh", ["-c", `lsof -ti:${port} | xargs kill -9`], { stdio: "inherit" });
|
|
574
605
|
return new Promise((resolve) => {
|
|
575
|
-
killProcess.on(
|
|
606
|
+
killProcess.on("exit", (code) => {
|
|
576
607
|
if (code === 0) {
|
|
577
608
|
console.log(chalk.green(`ā
Killed ${name} on port ${port}`));
|
|
578
609
|
}
|
|
@@ -580,48 +611,48 @@ export class DevEnvironment {
|
|
|
580
611
|
});
|
|
581
612
|
});
|
|
582
613
|
}
|
|
583
|
-
catch (
|
|
614
|
+
catch (_error) {
|
|
584
615
|
console.log(chalk.gray(`ā ļø Could not kill ${name} on port ${port}`));
|
|
585
616
|
}
|
|
586
617
|
};
|
|
587
618
|
// Kill servers
|
|
588
|
-
console.log(chalk.
|
|
619
|
+
console.log(chalk.cyan("š Killing servers..."));
|
|
589
620
|
await Promise.all([
|
|
590
|
-
killPortProcess(this.options.port,
|
|
591
|
-
killPortProcess(this.options.mcpPort,
|
|
621
|
+
killPortProcess(this.options.port, "your app server"),
|
|
622
|
+
killPortProcess(this.options.mcpPort, `${this.options.commandName} MCP server`),
|
|
592
623
|
]);
|
|
593
|
-
// Shutdown CDP monitor
|
|
624
|
+
// Shutdown CDP monitor if it was started
|
|
594
625
|
if (this.cdpMonitor) {
|
|
595
626
|
try {
|
|
596
|
-
console.log(chalk.
|
|
627
|
+
console.log(chalk.cyan("š Closing CDP monitor..."));
|
|
597
628
|
await this.cdpMonitor.shutdown();
|
|
598
|
-
console.log(chalk.green(
|
|
629
|
+
console.log(chalk.green("ā
CDP monitor closed"));
|
|
599
630
|
}
|
|
600
|
-
catch (
|
|
601
|
-
console.log(chalk.gray(
|
|
631
|
+
catch (_error) {
|
|
632
|
+
console.log(chalk.gray("ā ļø CDP monitor shutdown failed"));
|
|
602
633
|
}
|
|
603
634
|
}
|
|
604
|
-
console.log(chalk.red(
|
|
635
|
+
console.log(chalk.red(`ā ${this.options.commandName} exited due to server failure`));
|
|
605
636
|
process.exit(1);
|
|
606
637
|
}
|
|
607
638
|
setupCleanupHandlers() {
|
|
608
639
|
// Handle Ctrl+C to kill all processes
|
|
609
|
-
process.on(
|
|
640
|
+
process.on("SIGINT", async () => {
|
|
610
641
|
if (this.isShuttingDown)
|
|
611
642
|
return; // Prevent multiple shutdown attempts
|
|
612
643
|
this.isShuttingDown = true;
|
|
613
644
|
// Stop spinner if it's running
|
|
614
|
-
if (this.spinner
|
|
615
|
-
this.spinner.fail(
|
|
645
|
+
if (this.spinner?.isSpinning) {
|
|
646
|
+
this.spinner.fail("Interrupted");
|
|
616
647
|
}
|
|
617
|
-
console.log(chalk.yellow(
|
|
648
|
+
console.log(chalk.yellow("\nš Received interrupt signal. Cleaning up processes..."));
|
|
618
649
|
// Kill processes on both ports FIRST - this is most important
|
|
619
650
|
const killPortProcess = async (port, name) => {
|
|
620
651
|
try {
|
|
621
|
-
const { spawn } = await import(
|
|
622
|
-
const killProcess = spawn(
|
|
652
|
+
const { spawn } = await import("child_process");
|
|
653
|
+
const killProcess = spawn("sh", ["-c", `lsof -ti:${port} | xargs kill -9`], { stdio: "inherit" });
|
|
623
654
|
return new Promise((resolve) => {
|
|
624
|
-
killProcess.on(
|
|
655
|
+
killProcess.on("exit", (code) => {
|
|
625
656
|
if (code === 0) {
|
|
626
657
|
console.log(chalk.green(`ā
Killed ${name} on port ${port}`));
|
|
627
658
|
}
|
|
@@ -629,28 +660,28 @@ export class DevEnvironment {
|
|
|
629
660
|
});
|
|
630
661
|
});
|
|
631
662
|
}
|
|
632
|
-
catch (
|
|
663
|
+
catch (_error) {
|
|
633
664
|
console.log(chalk.gray(`ā ļø Could not kill ${name} on port ${port}`));
|
|
634
665
|
}
|
|
635
666
|
};
|
|
636
667
|
// Kill servers immediately - don't wait for browser cleanup
|
|
637
|
-
console.log(chalk.
|
|
668
|
+
console.log(chalk.yellow("š Killing servers..."));
|
|
638
669
|
await Promise.all([
|
|
639
|
-
killPortProcess(this.options.port,
|
|
640
|
-
killPortProcess(this.options.mcpPort,
|
|
670
|
+
killPortProcess(this.options.port, "your app server"),
|
|
671
|
+
killPortProcess(this.options.mcpPort, `${this.options.commandName} MCP server`),
|
|
641
672
|
]);
|
|
642
|
-
// Shutdown CDP monitor
|
|
673
|
+
// Shutdown CDP monitor if it was started
|
|
643
674
|
if (this.cdpMonitor) {
|
|
644
675
|
try {
|
|
645
|
-
console.log(chalk.
|
|
676
|
+
console.log(chalk.cyan("š Closing CDP monitor..."));
|
|
646
677
|
await this.cdpMonitor.shutdown();
|
|
647
|
-
console.log(chalk.green(
|
|
678
|
+
console.log(chalk.green("ā
CDP monitor closed"));
|
|
648
679
|
}
|
|
649
|
-
catch (
|
|
650
|
-
console.log(chalk.gray(
|
|
680
|
+
catch (_error) {
|
|
681
|
+
console.log(chalk.gray("ā ļø CDP monitor shutdown failed"));
|
|
651
682
|
}
|
|
652
683
|
}
|
|
653
|
-
console.log(chalk.green(
|
|
684
|
+
console.log(chalk.green("ā
Cleanup complete"));
|
|
654
685
|
process.exit(0);
|
|
655
686
|
});
|
|
656
687
|
}
|