@superdangerous/app-framework 4.9.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/LICENSE +21 -0
- package/README.md +652 -0
- package/dist/api/logsRouter.d.ts +20 -0
- package/dist/api/logsRouter.d.ts.map +1 -0
- package/dist/api/logsRouter.js +515 -0
- package/dist/api/logsRouter.js.map +1 -0
- package/dist/cli/dev-server.d.ts +7 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +640 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +26 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/StandardServer.d.ts +129 -0
- package/dist/core/StandardServer.d.ts.map +1 -0
- package/dist/core/StandardServer.js +453 -0
- package/dist/core/StandardServer.js.map +1 -0
- package/dist/core/apiResponse.d.ts +69 -0
- package/dist/core/apiResponse.d.ts.map +1 -0
- package/dist/core/apiResponse.js +127 -0
- package/dist/core/apiResponse.js.map +1 -0
- package/dist/core/healthCheck.d.ts +160 -0
- package/dist/core/healthCheck.d.ts.map +1 -0
- package/dist/core/healthCheck.js +398 -0
- package/dist/core/healthCheck.js.map +1 -0
- package/dist/core/index.d.ts +40 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +40 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +117 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +826 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/portUtils.d.ts +71 -0
- package/dist/core/portUtils.d.ts.map +1 -0
- package/dist/core/portUtils.js +240 -0
- package/dist/core/portUtils.js.map +1 -0
- package/dist/core/storageService.d.ts +119 -0
- package/dist/core/storageService.d.ts.map +1 -0
- package/dist/core/storageService.js +405 -0
- package/dist/core/storageService.js.map +1 -0
- package/dist/desktop/bundler.d.ts +40 -0
- package/dist/desktop/bundler.d.ts.map +1 -0
- package/dist/desktop/bundler.js +176 -0
- package/dist/desktop/bundler.js.map +1 -0
- package/dist/desktop/index.d.ts +25 -0
- package/dist/desktop/index.d.ts.map +1 -0
- package/dist/desktop/index.js +15 -0
- package/dist/desktop/index.js.map +1 -0
- package/dist/desktop/native-modules.d.ts +66 -0
- package/dist/desktop/native-modules.d.ts.map +1 -0
- package/dist/desktop/native-modules.js +200 -0
- package/dist/desktop/native-modules.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/LogCategories.d.ts +87 -0
- package/dist/logging/LogCategories.d.ts.map +1 -0
- package/dist/logging/LogCategories.js +205 -0
- package/dist/logging/LogCategories.js.map +1 -0
- package/dist/middleware/aiErrorHandler.d.ts +31 -0
- package/dist/middleware/aiErrorHandler.d.ts.map +1 -0
- package/dist/middleware/aiErrorHandler.js +181 -0
- package/dist/middleware/aiErrorHandler.js.map +1 -0
- package/dist/middleware/auth.d.ts +101 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +230 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +56 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +123 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +13 -0
- package/dist/middleware/errorHandler.d.ts.map +1 -0
- package/dist/middleware/errorHandler.js +85 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/fileUpload.d.ts +62 -0
- package/dist/middleware/fileUpload.d.ts.map +1 -0
- package/dist/middleware/fileUpload.js +175 -0
- package/dist/middleware/fileUpload.js.map +1 -0
- package/dist/middleware/health.d.ts +48 -0
- package/dist/middleware/health.d.ts.map +1 -0
- package/dist/middleware/health.js +143 -0
- package/dist/middleware/health.js.map +1 -0
- package/dist/middleware/index.d.ts +20 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +18 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/openapi.d.ts +64 -0
- package/dist/middleware/openapi.d.ts.map +1 -0
- package/dist/middleware/openapi.js +258 -0
- package/dist/middleware/openapi.js.map +1 -0
- package/dist/middleware/requestLogging.d.ts +22 -0
- package/dist/middleware/requestLogging.d.ts.map +1 -0
- package/dist/middleware/requestLogging.js +61 -0
- package/dist/middleware/requestLogging.js.map +1 -0
- package/dist/middleware/session.d.ts +84 -0
- package/dist/middleware/session.d.ts.map +1 -0
- package/dist/middleware/session.js +189 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/validation.d.ts +1337 -0
- package/dist/middleware/validation.d.ts.map +1 -0
- package/dist/middleware/validation.js +483 -0
- package/dist/middleware/validation.js.map +1 -0
- package/dist/services/aiService.d.ts +180 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +547 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/conversationStorage.d.ts +38 -0
- package/dist/services/conversationStorage.d.ts.map +1 -0
- package/dist/services/conversationStorage.js +158 -0
- package/dist/services/conversationStorage.js.map +1 -0
- package/dist/services/crossPlatformBuffer.d.ts +84 -0
- package/dist/services/crossPlatformBuffer.d.ts.map +1 -0
- package/dist/services/crossPlatformBuffer.js +246 -0
- package/dist/services/crossPlatformBuffer.js.map +1 -0
- package/dist/services/index.d.ts +17 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +18 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/networkService.d.ts +81 -0
- package/dist/services/networkService.d.ts.map +1 -0
- package/dist/services/networkService.js +268 -0
- package/dist/services/networkService.js.map +1 -0
- package/dist/services/queueService.d.ts +112 -0
- package/dist/services/queueService.d.ts.map +1 -0
- package/dist/services/queueService.js +338 -0
- package/dist/services/queueService.js.map +1 -0
- package/dist/services/settingsService.d.ts +135 -0
- package/dist/services/settingsService.d.ts.map +1 -0
- package/dist/services/settingsService.js +425 -0
- package/dist/services/settingsService.js.map +1 -0
- package/dist/services/systemMonitor.d.ts +208 -0
- package/dist/services/systemMonitor.d.ts.map +1 -0
- package/dist/services/systemMonitor.js +693 -0
- package/dist/services/systemMonitor.js.map +1 -0
- package/dist/services/updateService.d.ts +78 -0
- package/dist/services/updateService.d.ts.map +1 -0
- package/dist/services/updateService.js +252 -0
- package/dist/services/updateService.js.map +1 -0
- package/dist/services/websocketEvents.d.ts +372 -0
- package/dist/services/websocketEvents.d.ts.map +1 -0
- package/dist/services/websocketEvents.js +338 -0
- package/dist/services/websocketEvents.js.map +1 -0
- package/dist/services/websocketServer.d.ts +80 -0
- package/dist/services/websocketServer.d.ts.map +1 -0
- package/dist/services/websocketServer.js +299 -0
- package/dist/services/websocketServer.js.map +1 -0
- package/dist/settings/SettingsSchema.d.ts +151 -0
- package/dist/settings/SettingsSchema.d.ts.map +1 -0
- package/dist/settings/SettingsSchema.js +424 -0
- package/dist/settings/SettingsSchema.js.map +1 -0
- package/dist/testing/TestServer.d.ts +69 -0
- package/dist/testing/TestServer.d.ts.map +1 -0
- package/dist/testing/TestServer.js +250 -0
- package/dist/testing/TestServer.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/appPaths.d.ts +74 -0
- package/dist/utils/appPaths.d.ts.map +1 -0
- package/dist/utils/appPaths.js +162 -0
- package/dist/utils/appPaths.js.map +1 -0
- package/dist/utils/fs-utils.d.ts +50 -0
- package/dist/utils/fs-utils.d.ts.map +1 -0
- package/dist/utils/fs-utils.js +114 -0
- package/dist/utils/fs-utils.js.map +1 -0
- package/dist/utils/index.d.ts +12 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +10 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/standardConfig.d.ts +61 -0
- package/dist/utils/standardConfig.d.ts.map +1 -0
- package/dist/utils/standardConfig.js +109 -0
- package/dist/utils/standardConfig.js.map +1 -0
- package/dist/utils/startupBanner.d.ts +34 -0
- package/dist/utils/startupBanner.d.ts.map +1 -0
- package/dist/utils/startupBanner.js +169 -0
- package/dist/utils/startupBanner.js.map +1 -0
- package/dist/utils/startupLogger.d.ts +45 -0
- package/dist/utils/startupLogger.d.ts.map +1 -0
- package/dist/utils/startupLogger.js +200 -0
- package/dist/utils/startupLogger.js.map +1 -0
- package/package.json +151 -0
- package/src/api/logsRouter.ts +600 -0
- package/src/cli/dev-server.ts +803 -0
- package/src/cli/index.ts +31 -0
- package/src/core/StandardServer.ts +587 -0
- package/src/core/apiResponse.ts +202 -0
- package/src/core/healthCheck.ts +565 -0
- package/src/core/index.ts +80 -0
- package/src/core/logger.ts +1092 -0
- package/src/core/portUtils.ts +319 -0
- package/src/core/storageService.ts +595 -0
- package/src/desktop/bundler.ts +271 -0
- package/src/desktop/index.ts +18 -0
- package/src/desktop/native-modules.ts +289 -0
- package/src/index.ts +142 -0
- package/src/logging/LogCategories.ts +302 -0
- package/src/middleware/aiErrorHandler.ts +278 -0
- package/src/middleware/auth.ts +329 -0
- package/src/middleware/cors.ts +187 -0
- package/src/middleware/errorHandler.ts +103 -0
- package/src/middleware/fileUpload.ts +252 -0
- package/src/middleware/health.ts +206 -0
- package/src/middleware/index.ts +71 -0
- package/src/middleware/openapi.ts +305 -0
- package/src/middleware/requestLogging.ts +92 -0
- package/src/middleware/session.ts +238 -0
- package/src/middleware/validation.ts +603 -0
- package/src/services/aiService.ts +789 -0
- package/src/services/conversationStorage.ts +232 -0
- package/src/services/crossPlatformBuffer.ts +341 -0
- package/src/services/index.ts +47 -0
- package/src/services/networkService.ts +351 -0
- package/src/services/queueService.ts +446 -0
- package/src/services/settingsService.ts +549 -0
- package/src/services/systemMonitor.ts +936 -0
- package/src/services/updateService.ts +334 -0
- package/src/services/websocketEvents.ts +409 -0
- package/src/services/websocketServer.ts +394 -0
- package/src/settings/SettingsSchema.ts +664 -0
- package/src/testing/TestServer.ts +312 -0
- package/src/types/index.ts +154 -0
- package/src/utils/appPaths.ts +196 -0
- package/src/utils/fs-utils.ts +130 -0
- package/src/utils/index.ts +15 -0
- package/src/utils/standardConfig.ts +178 -0
- package/src/utils/startupBanner.ts +287 -0
- package/src/utils/startupLogger.ts +268 -0
- package/ui/dist/index.d.mts +1221 -0
- package/ui/dist/index.d.ts +1221 -0
- package/ui/dist/index.js +73 -0
- package/ui/dist/index.js.map +1 -0
- package/ui/dist/index.mjs +73 -0
- package/ui/dist/index.mjs.map +1 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Development server orchestrator for framework apps
|
|
4
|
+
* Manages both backend and frontend processes with unified output
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import net from "net";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import boxen from "boxen";
|
|
12
|
+
import { displayStartupBanner } from "../utils/startupBanner.js";
|
|
13
|
+
import { createLogger } from "../core/logger.js";
|
|
14
|
+
const logger = createLogger("dev-server");
|
|
15
|
+
class DevServerOrchestrator {
|
|
16
|
+
config;
|
|
17
|
+
backendProcess = null;
|
|
18
|
+
frontendProcess = null;
|
|
19
|
+
startTime;
|
|
20
|
+
isBackendReady = null;
|
|
21
|
+
isFrontendReady = false;
|
|
22
|
+
hasDetectedBackendReady = false;
|
|
23
|
+
hasDetectedFrontendReady = false;
|
|
24
|
+
hasShownBanner = false;
|
|
25
|
+
retryCount = 0;
|
|
26
|
+
outputBuffer = [];
|
|
27
|
+
frontendError = null;
|
|
28
|
+
constructor() {
|
|
29
|
+
// Load config from package.json
|
|
30
|
+
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
31
|
+
// Smart defaults based on common patterns
|
|
32
|
+
const defaultBackendCommand = fs.existsSync("src/server/index.ts")
|
|
33
|
+
? "tsx watch src/server/index.ts"
|
|
34
|
+
: fs.existsSync("src/index.ts")
|
|
35
|
+
? "tsx watch src/index.ts"
|
|
36
|
+
: "tsx watch src/server.ts";
|
|
37
|
+
const defaultFrontendCommand = fs.existsSync("web/package.json")
|
|
38
|
+
? "cd web && npm run dev"
|
|
39
|
+
: "vite";
|
|
40
|
+
// Get ports from environment or package.json or defaults
|
|
41
|
+
const defaultBackendPort = process.env.PORT
|
|
42
|
+
? parseInt(process.env.PORT)
|
|
43
|
+
: packageJson.devServer?.backendPort || 3000;
|
|
44
|
+
this.config = {
|
|
45
|
+
appName: packageJson.name || "SuperDangerous App",
|
|
46
|
+
appVersion: packageJson.version || "0.0.0",
|
|
47
|
+
packageName: packageJson.name,
|
|
48
|
+
description: packageJson.description,
|
|
49
|
+
backendPort: defaultBackendPort,
|
|
50
|
+
frontendPort: packageJson.devServer?.frontendPort || 5173,
|
|
51
|
+
webSocketPort: packageJson.devServer?.webSocketPort || defaultBackendPort,
|
|
52
|
+
backendCommand: packageJson.devServer?.backendCommand || defaultBackendCommand,
|
|
53
|
+
frontendCommand: packageJson.devServer?.frontendCommand || defaultFrontendCommand,
|
|
54
|
+
};
|
|
55
|
+
// Set process title for easier identification
|
|
56
|
+
process.title = `dev-server:${this.config.appName}:${this.config.backendPort}`;
|
|
57
|
+
this.startTime = Date.now();
|
|
58
|
+
}
|
|
59
|
+
showStartupBanner() {
|
|
60
|
+
// Only print once both services are ready
|
|
61
|
+
if (!this.isBackendReady || !this.isFrontendReady || this.hasShownBanner)
|
|
62
|
+
return;
|
|
63
|
+
logger.info("Clearing console for startup banner");
|
|
64
|
+
console.clear();
|
|
65
|
+
// Use the standardized banner from utils
|
|
66
|
+
displayStartupBanner({
|
|
67
|
+
appName: this.config.appName,
|
|
68
|
+
appVersion: this.config.appVersion,
|
|
69
|
+
packageName: this.config.packageName,
|
|
70
|
+
description: this.config.description,
|
|
71
|
+
port: this.config.backendPort,
|
|
72
|
+
webPort: this.config.frontendPort,
|
|
73
|
+
webSocketPort: this.config.webSocketPort,
|
|
74
|
+
environment: process.env.NODE_ENV || "development",
|
|
75
|
+
startTime: this.startTime,
|
|
76
|
+
});
|
|
77
|
+
this.hasShownBanner = true;
|
|
78
|
+
// Show any buffered important messages
|
|
79
|
+
if (this.outputBuffer.length > 0) {
|
|
80
|
+
logger.info("\n" + chalk.gray("Recent activity:"));
|
|
81
|
+
this.outputBuffer.forEach((msg) => logger.info(msg));
|
|
82
|
+
this.outputBuffer = [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
parseCommand(command) {
|
|
86
|
+
const parts = command.split(" ");
|
|
87
|
+
return {
|
|
88
|
+
cmd: parts[0],
|
|
89
|
+
args: parts.slice(1),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
startBackend() {
|
|
93
|
+
logger.info(chalk.gray("Starting backend on port " + this.config.backendPort + "..."));
|
|
94
|
+
const { cmd, args } = this.parseCommand(this.config.backendCommand);
|
|
95
|
+
this.backendProcess = spawn(cmd, args, {
|
|
96
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
97
|
+
shell: true,
|
|
98
|
+
detached: true, // Create a new process group
|
|
99
|
+
env: {
|
|
100
|
+
...process.env,
|
|
101
|
+
LOG_LEVEL: "warn", // Only show warnings and errors
|
|
102
|
+
NODE_ENV: "development",
|
|
103
|
+
FORCE_COLOR: "3",
|
|
104
|
+
PORT: this.config.backendPort.toString(),
|
|
105
|
+
APP_NAME: this.config.appName,
|
|
106
|
+
APP_PORT: this.config.backendPort.toString(),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
this.backendProcess.stdout?.on("data", (data) => {
|
|
110
|
+
const output = data.toString();
|
|
111
|
+
// Look for ANY indication the server is ready
|
|
112
|
+
if (!this.hasDetectedBackendReady) {
|
|
113
|
+
// Check for various ready indicators
|
|
114
|
+
if (output.includes("Ready in") ||
|
|
115
|
+
output.includes("Server is running") ||
|
|
116
|
+
output.includes("Server started") ||
|
|
117
|
+
output.includes("Listening on") ||
|
|
118
|
+
output.includes(`${this.config.backendPort}`) ||
|
|
119
|
+
output.includes("✓")) {
|
|
120
|
+
this.hasDetectedBackendReady = true;
|
|
121
|
+
this.isBackendReady = true;
|
|
122
|
+
this.showStartupBanner();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Check for port conflict and show enhanced error
|
|
127
|
+
if (output.includes("EADDRINUSE") || output.includes("already in use")) {
|
|
128
|
+
this.showPortConflictError(this.config.backendPort, "backend");
|
|
129
|
+
}
|
|
130
|
+
// Show errors
|
|
131
|
+
if (output.toLowerCase().includes("error") &&
|
|
132
|
+
!output.includes("ExperimentalWarning")) {
|
|
133
|
+
const lines = output.trim().split("\n");
|
|
134
|
+
lines.forEach((line) => {
|
|
135
|
+
if (line.trim()) {
|
|
136
|
+
const msg = chalk.red(`[Backend] ${line.trim()}`);
|
|
137
|
+
if (this.hasShownBanner) {
|
|
138
|
+
logger.error(msg);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
this.outputBuffer.push(msg);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
this.backendProcess.stderr?.on("data", (data) => {
|
|
148
|
+
const output = data.toString();
|
|
149
|
+
// Check for port conflict
|
|
150
|
+
if (output.includes("EADDRINUSE") ||
|
|
151
|
+
output.includes("address already in use")) {
|
|
152
|
+
this.showPortConflictError(this.config.backendPort, "backend");
|
|
153
|
+
}
|
|
154
|
+
// For backend stderr, just pass it through as-is to preserve formatting
|
|
155
|
+
// The backend logger already handles coloring appropriately
|
|
156
|
+
if (output.trim() && !output.includes("ExperimentalWarning")) {
|
|
157
|
+
process.stderr.write(output);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Secondary readiness check: wait for port to open in case stdout is quiet
|
|
161
|
+
this.waitForPort(this.config.backendPort, 8000).then((reachable) => {
|
|
162
|
+
if (!this.hasDetectedBackendReady && reachable) {
|
|
163
|
+
this.hasDetectedBackendReady = true;
|
|
164
|
+
this.isBackendReady = true;
|
|
165
|
+
this.showStartupBanner();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this.backendProcess.on("error", (error) => {
|
|
169
|
+
logger.error(chalk.red(`[Backend] Failed to start: ${error.message}`));
|
|
170
|
+
});
|
|
171
|
+
this.backendProcess.on("exit", (code) => {
|
|
172
|
+
if (code !== 0 && code !== null) {
|
|
173
|
+
this.isBackendReady = false;
|
|
174
|
+
logger.error(chalk.red(`[Backend] Exited with code ${code}`));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
// Fallback: Mark as ready after timeout if we haven't detected it
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
if (!this.hasDetectedBackendReady) {
|
|
180
|
+
if (this.isBackendReady !== false) {
|
|
181
|
+
logger.warn(chalk.yellow("Backend ready detection timeout - assuming ready"));
|
|
182
|
+
this.hasDetectedBackendReady = true;
|
|
183
|
+
this.isBackendReady = true;
|
|
184
|
+
this.showStartupBanner();
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
logger.error(chalk.red("\nBackend failed to start. Check the error messages above."));
|
|
188
|
+
this.cleanup();
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}, 5000);
|
|
193
|
+
}
|
|
194
|
+
async startFrontend() {
|
|
195
|
+
logger.info(chalk.gray("Starting frontend on port " + this.config.frontendPort + "..."));
|
|
196
|
+
// Quick check if port is available before spawning process
|
|
197
|
+
// This helps catch port conflicts faster than waiting for Vite to report them
|
|
198
|
+
const portAvailable = await this.checkPort(this.config.frontendPort);
|
|
199
|
+
if (!portAvailable) {
|
|
200
|
+
this.frontendError = `Port ${this.config.frontendPort} is already in use`;
|
|
201
|
+
// Don't call showPortConflictError here as it exits immediately
|
|
202
|
+
// Instead, simulate a frontend exit to trigger the error banner
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
if (this.frontendProcess) {
|
|
205
|
+
this.frontendProcess.emit("exit", 1);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// If no process created yet, show error directly
|
|
209
|
+
console.clear();
|
|
210
|
+
logger.error(boxen(chalk.red("⚠️ Frontend Failed to Start!\n\n") +
|
|
211
|
+
chalk.white(this.frontendError + "\n\n") +
|
|
212
|
+
chalk.gray("To fix this issue:\n") +
|
|
213
|
+
chalk.gray(`1. Stop the other process using port ${this.config.frontendPort}\n`) +
|
|
214
|
+
chalk.gray(`2. Check what's using it: ${chalk.cyan(`lsof -i :${this.config.frontendPort}`)}\n`) +
|
|
215
|
+
chalk.gray(`3. Kill it with: ${chalk.cyan(`kill -9 <PID>`)}\n`) +
|
|
216
|
+
chalk.gray(`4. Or use a different port in your configuration`), {
|
|
217
|
+
padding: 1,
|
|
218
|
+
margin: 1,
|
|
219
|
+
borderStyle: "round",
|
|
220
|
+
borderColor: "red",
|
|
221
|
+
}));
|
|
222
|
+
this.cleanup();
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}, 100);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Handle different frontend command formats
|
|
229
|
+
let cmd;
|
|
230
|
+
let args;
|
|
231
|
+
if (this.config.frontendCommand.includes("cd ")) {
|
|
232
|
+
// Complex command like "cd web && npm run dev"
|
|
233
|
+
// Extract the directory and command
|
|
234
|
+
const cdMatch = this.config.frontendCommand.match(/cd\s+([^\s&]+)\s*&&\s*(.+)/);
|
|
235
|
+
if (cdMatch) {
|
|
236
|
+
const [, dir, actualCmd] = cdMatch;
|
|
237
|
+
// Run the command in the specified directory
|
|
238
|
+
const parsed = this.parseCommand(actualCmd);
|
|
239
|
+
cmd = parsed.cmd;
|
|
240
|
+
args = parsed.args;
|
|
241
|
+
// Override cwd below to run in the subdirectory
|
|
242
|
+
this.frontendProcess = spawn(cmd, args, {
|
|
243
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
244
|
+
shell: true,
|
|
245
|
+
detached: true,
|
|
246
|
+
cwd: path.join(process.cwd(), dir), // Run in the subdirectory
|
|
247
|
+
env: {
|
|
248
|
+
...process.env,
|
|
249
|
+
NODE_ENV: "development",
|
|
250
|
+
FORCE_COLOR: "3",
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Fallback to sh -c for complex commands
|
|
256
|
+
cmd = "sh";
|
|
257
|
+
args = ["-c", this.config.frontendCommand];
|
|
258
|
+
logger.info(chalk.gray(`Frontend command: ${cmd} ${args.join(" ")}`));
|
|
259
|
+
this.frontendProcess = spawn(cmd, args, {
|
|
260
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
261
|
+
shell: true,
|
|
262
|
+
detached: true,
|
|
263
|
+
cwd: process.cwd(),
|
|
264
|
+
env: {
|
|
265
|
+
...process.env,
|
|
266
|
+
NODE_ENV: "development",
|
|
267
|
+
FORCE_COLOR: "3",
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else if (this.config.frontendCommand === "vite") {
|
|
273
|
+
// Direct vite command
|
|
274
|
+
cmd = "npx";
|
|
275
|
+
args = ["vite", "--port", this.config.frontendPort.toString()];
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// Parse other commands
|
|
279
|
+
const parsed = this.parseCommand(this.config.frontendCommand);
|
|
280
|
+
cmd = parsed.cmd;
|
|
281
|
+
args = parsed.args;
|
|
282
|
+
}
|
|
283
|
+
// Only spawn if not already created above for cd commands
|
|
284
|
+
if (!this.frontendProcess) {
|
|
285
|
+
this.frontendProcess = spawn(cmd, args, {
|
|
286
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
287
|
+
shell: true,
|
|
288
|
+
detached: true, // Create a new process group
|
|
289
|
+
cwd: process.cwd(), // Explicitly set working directory
|
|
290
|
+
env: {
|
|
291
|
+
...process.env,
|
|
292
|
+
NODE_ENV: "development",
|
|
293
|
+
FORCE_COLOR: "3",
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
this.frontendProcess.stdout?.on("data", (data) => {
|
|
298
|
+
const output = data.toString();
|
|
299
|
+
// Debug: Log all frontend output
|
|
300
|
+
if (process.env.DEBUG_DEV_SERVER) {
|
|
301
|
+
logger.info(chalk.blue(`[Frontend stdout] ${output.trim()}`));
|
|
302
|
+
}
|
|
303
|
+
// Detect when frontend is ready
|
|
304
|
+
if (!this.hasDetectedFrontendReady) {
|
|
305
|
+
if (output.includes("ready in") ||
|
|
306
|
+
output.includes("Local:") ||
|
|
307
|
+
output.includes("➜") ||
|
|
308
|
+
output.includes(`${this.config.frontendPort}`)) {
|
|
309
|
+
// Extract actual port if different
|
|
310
|
+
const portMatch = output.match(/localhost:(\d+)/);
|
|
311
|
+
if (portMatch) {
|
|
312
|
+
this.config.frontendPort = parseInt(portMatch[1]);
|
|
313
|
+
}
|
|
314
|
+
this.hasDetectedFrontendReady = true;
|
|
315
|
+
this.isFrontendReady = true;
|
|
316
|
+
this.showStartupBanner();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Check for port conflict
|
|
321
|
+
if (output.includes("EADDRINUSE") ||
|
|
322
|
+
output.includes("already in use") ||
|
|
323
|
+
output.includes("Please stop the other process")) {
|
|
324
|
+
// Store error for the exit handler
|
|
325
|
+
this.frontendError = `Port ${this.config.frontendPort} is already in use`;
|
|
326
|
+
logger.error(chalk.red(`\n⚠️ Port ${this.config.frontendPort} is already in use!`));
|
|
327
|
+
logger.error(chalk.yellow("Please stop the other process or use a different port.\n"));
|
|
328
|
+
this.cleanup();
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
// Show errors
|
|
332
|
+
if (output.toLowerCase().includes("error")) {
|
|
333
|
+
const lines = output.trim().split("\n");
|
|
334
|
+
lines.forEach((line) => {
|
|
335
|
+
if (line.trim() && !line.includes("➜")) {
|
|
336
|
+
// Strip existing ANSI codes to prevent corruption
|
|
337
|
+
// eslint-disable-next-line no-control-regex
|
|
338
|
+
const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
339
|
+
const msg = chalk.red(`[Frontend] ${cleanLine}`);
|
|
340
|
+
if (this.hasShownBanner) {
|
|
341
|
+
logger.error(msg);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
this.outputBuffer.push(msg);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
this.frontendProcess.stderr?.on("data", (data) => {
|
|
351
|
+
const output = data.toString().trim();
|
|
352
|
+
if (output) {
|
|
353
|
+
// Debug: Log all frontend stderr
|
|
354
|
+
if (process.env.DEBUG_DEV_SERVER) {
|
|
355
|
+
logger.info(chalk.yellow(`[Frontend stderr] ${output}`));
|
|
356
|
+
}
|
|
357
|
+
// Check for Vite's port conflict error message
|
|
358
|
+
// Vite outputs: "Error: Port XXXX is already in use"
|
|
359
|
+
if (output.includes("Error:") && output.includes("is already in use")) {
|
|
360
|
+
// Extract port number from error message if possible
|
|
361
|
+
const portMatch = output.match(/Port (\d+)/);
|
|
362
|
+
const port = portMatch
|
|
363
|
+
? parseInt(portMatch[1])
|
|
364
|
+
: this.config.frontendPort;
|
|
365
|
+
this.frontendError = `Port ${port} is already in use`;
|
|
366
|
+
// Don't call showPortConflictError immediately as Vite will exit
|
|
367
|
+
// The exit handler will show the proper error banner
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Check for other port conflict formats
|
|
371
|
+
if ((output.includes("Port") && output.includes("is in use")) ||
|
|
372
|
+
output.includes("EADDRINUSE")) {
|
|
373
|
+
// Store error for the exit handler
|
|
374
|
+
this.frontendError = `Port ${this.config.frontendPort} is already in use`;
|
|
375
|
+
// Try to show the error immediately, but Vite might exit first
|
|
376
|
+
this.showPortConflictError(this.config.frontendPort, "frontend");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// Check if Vite is outputting its ready message to stderr
|
|
380
|
+
if (!this.hasDetectedFrontendReady) {
|
|
381
|
+
if (output.includes("ready in") ||
|
|
382
|
+
output.includes("Local:") ||
|
|
383
|
+
output.includes("➜") ||
|
|
384
|
+
output.includes(`${this.config.frontendPort}`)) {
|
|
385
|
+
// Extract actual port if different
|
|
386
|
+
const portMatch = output.match(/localhost:(\d+)/);
|
|
387
|
+
if (portMatch) {
|
|
388
|
+
this.config.frontendPort = parseInt(portMatch[1]);
|
|
389
|
+
}
|
|
390
|
+
this.hasDetectedFrontendReady = true;
|
|
391
|
+
this.isFrontendReady = true;
|
|
392
|
+
this.showStartupBanner();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// For important errors, show them immediately
|
|
397
|
+
if (output.toLowerCase().includes("error") &&
|
|
398
|
+
!output.includes("ExperimentalWarning")) {
|
|
399
|
+
logger.error(chalk.red(`[Frontend] ${output}`));
|
|
400
|
+
}
|
|
401
|
+
else if (!this.hasShownBanner) {
|
|
402
|
+
// Buffer other output until banner is shown
|
|
403
|
+
this.outputBuffer.push(output);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
// After banner, pass through as-is
|
|
407
|
+
process.stderr.write(output + "\n");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
this.frontendProcess.on("error", (error) => {
|
|
412
|
+
// EAGAIN errors are temporary resource issues, retry
|
|
413
|
+
if (error.code === "EAGAIN" && this.retryCount < 3) {
|
|
414
|
+
this.retryCount++;
|
|
415
|
+
logger.warn(chalk.yellow(`[Frontend] Resource temporarily unavailable, retrying (${this.retryCount}/3)...`));
|
|
416
|
+
setTimeout(async () => await this.startFrontend(), 1000 * this.retryCount);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// For other errors, show in yellow (warning) not red (error) since it might still work
|
|
420
|
+
logger.warn(chalk.yellow(`[Frontend] Process spawn warning: ${error.message}`));
|
|
421
|
+
if (process.env.DEBUG_DEV_SERVER) {
|
|
422
|
+
logger.info(chalk.gray(`[Frontend] Command: ${cmd} ${args.join(" ")}`));
|
|
423
|
+
logger.info(chalk.gray(`[Frontend] Directory: ${process.cwd()}`));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
this.frontendProcess.on("exit", (code) => {
|
|
427
|
+
if (code !== 0 && code !== null) {
|
|
428
|
+
// Show specific error if we captured one
|
|
429
|
+
if (this.frontendError) {
|
|
430
|
+
console.clear();
|
|
431
|
+
logger.error(boxen(chalk.red("⚠️ Frontend Failed to Start!\n\n") +
|
|
432
|
+
chalk.white(this.frontendError + "\n\n") +
|
|
433
|
+
chalk.gray("To fix this issue:\n") +
|
|
434
|
+
chalk.gray(`1. Stop the other process using port ${this.config.frontendPort}\n`) +
|
|
435
|
+
chalk.gray(`2. Check what's using it: ${chalk.cyan(`lsof -i :${this.config.frontendPort}`)}\n`) +
|
|
436
|
+
chalk.gray(`3. Kill it with: ${chalk.cyan(`kill -9 <PID>`)}\n`) +
|
|
437
|
+
chalk.gray(`4. Or use a different port in your configuration`), {
|
|
438
|
+
padding: 1,
|
|
439
|
+
margin: 1,
|
|
440
|
+
borderStyle: "round",
|
|
441
|
+
borderColor: "red",
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
logger.error(chalk.red(`[Frontend] Exited with code ${code}`));
|
|
446
|
+
if (!this.hasDetectedFrontendReady) {
|
|
447
|
+
logger.error(chalk.red(`[Frontend] Failed to start properly. Check that the frontend command works manually.`));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Exit the dev-server if frontend fails to start
|
|
451
|
+
if (!this.hasDetectedFrontendReady) {
|
|
452
|
+
this.cleanup();
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// Fallback: Mark as ready after timeout if we haven't detected it
|
|
458
|
+
setTimeout(() => {
|
|
459
|
+
if (!this.hasDetectedFrontendReady) {
|
|
460
|
+
logger.warn(chalk.yellow("Frontend ready detection timeout - assuming ready"));
|
|
461
|
+
this.hasDetectedFrontendReady = true;
|
|
462
|
+
this.isFrontendReady = true;
|
|
463
|
+
this.showStartupBanner();
|
|
464
|
+
}
|
|
465
|
+
}, 5000);
|
|
466
|
+
}
|
|
467
|
+
cleanup() {
|
|
468
|
+
// Kill process groups to ensure all child processes are terminated
|
|
469
|
+
if (this.backendProcess && this.backendProcess.pid) {
|
|
470
|
+
try {
|
|
471
|
+
// Kill the entire process group (negative PID)
|
|
472
|
+
process.kill(-this.backendProcess.pid, "SIGTERM");
|
|
473
|
+
}
|
|
474
|
+
catch (_e) {
|
|
475
|
+
// Fallback to regular kill
|
|
476
|
+
try {
|
|
477
|
+
this.backendProcess.kill("SIGTERM");
|
|
478
|
+
}
|
|
479
|
+
catch (_err) {
|
|
480
|
+
// Process might already be dead
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
this.backendProcess = null;
|
|
484
|
+
}
|
|
485
|
+
if (this.frontendProcess && this.frontendProcess.pid) {
|
|
486
|
+
try {
|
|
487
|
+
// Kill the entire process group (negative PID)
|
|
488
|
+
process.kill(-this.frontendProcess.pid, "SIGTERM");
|
|
489
|
+
}
|
|
490
|
+
catch (_e) {
|
|
491
|
+
// Fallback to regular kill
|
|
492
|
+
try {
|
|
493
|
+
this.frontendProcess.kill("SIGTERM");
|
|
494
|
+
}
|
|
495
|
+
catch (_err) {
|
|
496
|
+
// Process might already be dead
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
this.frontendProcess = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async checkPort(port) {
|
|
503
|
+
return new Promise((resolve) => {
|
|
504
|
+
const server = net.createServer();
|
|
505
|
+
server.once("error", (err) => {
|
|
506
|
+
if (err.code === "EADDRINUSE") {
|
|
507
|
+
resolve(false);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
resolve(true);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
server.once("listening", () => {
|
|
514
|
+
server.close();
|
|
515
|
+
resolve(true);
|
|
516
|
+
});
|
|
517
|
+
server.listen(port);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
async waitForPort(port, timeoutMs = 8000, host = "127.0.0.1") {
|
|
521
|
+
const start = Date.now();
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
const tryConnect = () => {
|
|
524
|
+
const socket = net.connect({ port, host }, () => {
|
|
525
|
+
socket.end();
|
|
526
|
+
resolve(true);
|
|
527
|
+
});
|
|
528
|
+
socket.on("error", () => {
|
|
529
|
+
socket.destroy();
|
|
530
|
+
if (Date.now() - start >= timeoutMs) {
|
|
531
|
+
resolve(false);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
setTimeout(tryConnect, 200);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
tryConnect();
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
async getProcessUsingPort(port) {
|
|
542
|
+
try {
|
|
543
|
+
const { execSync } = await import("child_process");
|
|
544
|
+
// Get PID using the port
|
|
545
|
+
const pidOutput = execSync(`lsof -i :${port} -P -t -sTCP:LISTEN 2>/dev/null || true`, { encoding: "utf-8" });
|
|
546
|
+
const pid = pidOutput.trim().split("\n")[0]; // Get first PID if multiple
|
|
547
|
+
if (!pid)
|
|
548
|
+
return null;
|
|
549
|
+
// Get full process command
|
|
550
|
+
const commandOutput = execSync(`ps -p ${pid} -o command= 2>/dev/null || echo "unknown"`, { encoding: "utf-8" }).trim();
|
|
551
|
+
// Simplify the command for display
|
|
552
|
+
let command = commandOutput;
|
|
553
|
+
if (command.includes("node")) {
|
|
554
|
+
if (command.includes("tsx"))
|
|
555
|
+
command = "tsx (TypeScript)";
|
|
556
|
+
else if (command.includes("vite"))
|
|
557
|
+
command = "vite (dev server)";
|
|
558
|
+
else if (command.includes("nodemon"))
|
|
559
|
+
command = "nodemon";
|
|
560
|
+
else
|
|
561
|
+
command = "node process";
|
|
562
|
+
}
|
|
563
|
+
return { pid, command };
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async showPortConflictError(port, portType = "backend") {
|
|
570
|
+
const processInfo = await this.getProcessUsingPort(port);
|
|
571
|
+
console.clear();
|
|
572
|
+
// Build the error message with subtle gray styling like the startup banner
|
|
573
|
+
let lines = [];
|
|
574
|
+
lines.push("");
|
|
575
|
+
lines.push(`⚠️ ${chalk.yellow(portType.charAt(0).toUpperCase() + portType.slice(1))} port ${chalk.yellow(port)} is already in use!`);
|
|
576
|
+
lines.push("");
|
|
577
|
+
if (processInfo) {
|
|
578
|
+
lines.push(chalk.gray(`Process: ${chalk.yellow(processInfo.command)}`));
|
|
579
|
+
lines.push(chalk.gray(`PID: ${chalk.yellow(processInfo.pid)}`));
|
|
580
|
+
lines.push("");
|
|
581
|
+
lines.push(chalk.gray("🔧 To fix this, run:"));
|
|
582
|
+
lines.push("");
|
|
583
|
+
lines.push(chalk.cyan(` kill -9 ${processInfo.pid}`));
|
|
584
|
+
lines.push("");
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
lines.push(chalk.gray("Could not identify the process using this port."));
|
|
588
|
+
lines.push("");
|
|
589
|
+
}
|
|
590
|
+
lines.push(chalk.gray("Other options:"));
|
|
591
|
+
lines.push(chalk.gray(`• Check what's using it: ${chalk.cyan(`lsof -i :${port}`)}`));
|
|
592
|
+
lines.push(chalk.gray(`• Kill all Node processes: ${chalk.cyan("pkill -f node")}`));
|
|
593
|
+
lines.push(chalk.gray(`• Use a different port in your package.json`));
|
|
594
|
+
lines.push("");
|
|
595
|
+
// Use subtle gray box like the startup banner
|
|
596
|
+
logger.error(boxen(lines.join("\n"), {
|
|
597
|
+
padding: 1,
|
|
598
|
+
margin: 1,
|
|
599
|
+
borderStyle: "round",
|
|
600
|
+
borderColor: "gray",
|
|
601
|
+
dimBorder: true,
|
|
602
|
+
}));
|
|
603
|
+
this.cleanup();
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
async start() {
|
|
607
|
+
logger.info(chalk.cyan(`\nStarting ${this.config.appName} development server...\n`));
|
|
608
|
+
// Check if backend port is available
|
|
609
|
+
const backendAvailable = await this.checkPort(this.config.backendPort);
|
|
610
|
+
if (!backendAvailable) {
|
|
611
|
+
await this.showPortConflictError(this.config.backendPort, "backend");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// Check if frontend port is available
|
|
615
|
+
const frontendAvailable = await this.checkPort(this.config.frontendPort);
|
|
616
|
+
if (!frontendAvailable) {
|
|
617
|
+
await this.showPortConflictError(this.config.frontendPort, "frontend");
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
// Start both processes
|
|
621
|
+
this.startBackend();
|
|
622
|
+
// Give backend a moment to start before frontend
|
|
623
|
+
setTimeout(async () => await this.startFrontend(), 1000);
|
|
624
|
+
// Handle graceful shutdown
|
|
625
|
+
process.on("SIGINT", () => {
|
|
626
|
+
logger.info(chalk.yellow("\n\nShutting down..."));
|
|
627
|
+
this.cleanup();
|
|
628
|
+
// Force exit after a delay if processes don't terminate
|
|
629
|
+
setTimeout(() => process.exit(0), 1000);
|
|
630
|
+
});
|
|
631
|
+
process.on("SIGTERM", () => {
|
|
632
|
+
this.cleanup();
|
|
633
|
+
process.exit(0);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Run the orchestrator
|
|
638
|
+
const orchestrator = new DevServerOrchestrator();
|
|
639
|
+
orchestrator.start();
|
|
640
|
+
//# sourceMappingURL=dev-server.js.map
|