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