@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,587 @@
1
+ /**
2
+ * Browser Console Streaming Implementation
3
+ *
4
+ * Captures console.* calls from the browser and streams them to the terminal,
5
+ * providing a unified debugging experience.
6
+ *
7
+ * @module frontend/console-stream
8
+ */
9
+
10
+ import { createLogger, type Logger } from "../logger/index.js";
11
+ import type {
12
+ ConsoleMessage,
13
+ ConsoleStreamConfig,
14
+ ConsoleStreamClient,
15
+ ConsoleClientMessage,
16
+ ConsoleServerMessage,
17
+ ConsoleMessageType,
18
+ PartialConsoleStreamConfig,
19
+ } from "./types.js";
20
+ import { CONSOLE_CLIENT_SCRIPT } from "./console-client.js";
21
+
22
+ // ============= Constants =============
23
+
24
+ const DEFAULT_PORT_OFFSET = 2; // Console stream port = dev server port + 2
25
+
26
+ // ANSI color codes for terminal output
27
+ const ANSI_COLORS = {
28
+ reset: "\x1b[0m",
29
+ bold: "\x1b[1m",
30
+ dim: "\x1b[2m",
31
+ white: "\x1b[37m",
32
+ gray: "\x1b[90m",
33
+ cyan: "\x1b[36m",
34
+ green: "\x1b[32m",
35
+ yellow: "\x1b[33m",
36
+ red: "\x1b[31m",
37
+ magenta: "\x1b[35m",
38
+ blue: "\x1b[34m",
39
+ };
40
+
41
+ // Console type colors
42
+ const CONSOLE_TYPE_COLORS: Record<ConsoleMessageType, string> = {
43
+ log: ANSI_COLORS.white,
44
+ info: ANSI_COLORS.cyan,
45
+ warn: ANSI_COLORS.yellow,
46
+ error: ANSI_COLORS.red,
47
+ debug: ANSI_COLORS.gray,
48
+ trace: ANSI_COLORS.magenta,
49
+ table: ANSI_COLORS.blue,
50
+ };
51
+
52
+ // ============= ConsoleStreamManager Class =============
53
+
54
+ /**
55
+ * Manages browser console streaming to the terminal.
56
+ *
57
+ * Features:
58
+ * - WebSocket server for receiving console messages from browser
59
+ * - Color-coded terminal output
60
+ * - File:line clickable links for VSCode
61
+ * - Object/array pretty printing
62
+ * - Stack traces for errors
63
+ * - Source map support for original file references
64
+ */
65
+ export class ConsoleStreamManager {
66
+ private config: ConsoleStreamConfig;
67
+ private logger: Logger;
68
+ private clients: Map<string, ConsoleStreamClient> = new Map();
69
+ private devServerPort: number;
70
+ private port: number;
71
+ private server: ReturnType<typeof Bun.serve> | null = null;
72
+
73
+ constructor(devServerPort: number, config?: PartialConsoleStreamConfig) {
74
+ this.devServerPort = devServerPort;
75
+ this.port = devServerPort + DEFAULT_PORT_OFFSET;
76
+ this.config = this.normalizeConfig(config);
77
+ this.logger = createLogger({
78
+ level: "debug",
79
+ pretty: true,
80
+ context: { component: "ConsoleStream" },
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Normalize partial config to full config with defaults
86
+ */
87
+ private normalizeConfig(config?: PartialConsoleStreamConfig): ConsoleStreamConfig {
88
+ return {
89
+ enabled: config?.enabled ?? true,
90
+ showTimestamps: config?.showTimestamps ?? true,
91
+ showFile: config?.showFile ?? true,
92
+ colorize: config?.colorize ?? true,
93
+ filter: config?.filter ?? ['log', 'info', 'warn', 'error', 'debug', 'trace', 'table'],
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Get the console client script for injection
99
+ */
100
+ getClientScript(): string {
101
+ return CONSOLE_CLIENT_SCRIPT;
102
+ }
103
+
104
+ /**
105
+ * Get the WebSocket URL for console streaming
106
+ */
107
+ getWebSocketUrl(): string {
108
+ return `ws://localhost:${this.port}/_console`;
109
+ }
110
+
111
+ /**
112
+ * Get the console stream port
113
+ */
114
+ getPort(): number {
115
+ return this.port;
116
+ }
117
+
118
+ /**
119
+ * Check if console streaming is enabled
120
+ */
121
+ isEnabled(): boolean {
122
+ return this.config.enabled;
123
+ }
124
+
125
+ /**
126
+ * Start the console stream WebSocket server
127
+ */
128
+ start(): void {
129
+ if (!this.config.enabled) {
130
+ return;
131
+ }
132
+
133
+ if (this.server) {
134
+ this.logger.warn("Console stream server already running");
135
+ return;
136
+ }
137
+
138
+ this.server = Bun.serve({
139
+ port: this.port,
140
+ fetch: this.handleFetch.bind(this),
141
+ websocket: {
142
+ open: this.handleOpen.bind(this),
143
+ close: this.handleClose.bind(this),
144
+ message: this.handleMessage.bind(this),
145
+ },
146
+ });
147
+
148
+ this.logger.info(`Console stream server started on port ${this.port}`);
149
+ }
150
+
151
+ /**
152
+ * Stop the console stream server
153
+ */
154
+ stop(): void {
155
+ if (this.server) {
156
+ // Disconnect all clients
157
+ for (const client of this.clients.values()) {
158
+ if (client.ws.readyState === WebSocket.OPEN) {
159
+ client.ws.close();
160
+ }
161
+ }
162
+ this.clients.clear();
163
+
164
+ this.server.stop();
165
+ this.server = null;
166
+ this.logger.info("Console stream server stopped");
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Handle fetch requests for WebSocket server
172
+ */
173
+ private handleFetch(request: Request, server: any): Response | undefined {
174
+ const url = new URL(request.url);
175
+
176
+ if (url.pathname === "/_console") {
177
+ const upgradeHeader = request.headers.get("upgrade");
178
+ if (upgradeHeader !== "websocket") {
179
+ return new Response("Expected WebSocket upgrade", { status: 426 });
180
+ }
181
+
182
+ const success = server.upgrade(request);
183
+ if (success) {
184
+ return undefined;
185
+ }
186
+ return new Response("WebSocket upgrade failed", { status: 400 });
187
+ }
188
+
189
+ return new Response("Not found", { status: 404 });
190
+ }
191
+
192
+ /**
193
+ * Handle WebSocket connection open
194
+ */
195
+ private handleOpen(ws: any): void {
196
+ const clientId = this.generateClientId();
197
+ const client: ConsoleStreamClient = {
198
+ id: clientId,
199
+ ws: ws,
200
+ };
201
+
202
+ this.clients.set(clientId, client);
203
+ ws.data = { clientId };
204
+
205
+ this.logger.debug(`Console client connected: ${clientId}`);
206
+
207
+ // Send connected message
208
+ this.sendToClient(ws, {
209
+ type: "connected",
210
+ clientId,
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Handle WebSocket connection close
216
+ */
217
+ private handleClose(ws: any): void {
218
+ const clientId = ws.data?.clientId;
219
+ if (clientId) {
220
+ this.clients.delete(clientId);
221
+ this.logger.debug(`Console client disconnected: ${clientId}`);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Handle WebSocket message from client
227
+ */
228
+ private handleMessage(ws: any, message: string | Buffer): void {
229
+ try {
230
+ const data: ConsoleClientMessage = JSON.parse(message.toString());
231
+
232
+ if (data.type === "console") {
233
+ // Update client URL if provided
234
+ const clientId = ws.data?.clientId;
235
+ if (clientId && data.url) {
236
+ const client = this.clients.get(clientId);
237
+ if (client) {
238
+ client.url = data.url;
239
+ }
240
+ }
241
+
242
+ // Process and display the console message
243
+ this.processConsoleMessage(data);
244
+ }
245
+ } catch (error) {
246
+ this.logger.error("Failed to parse console message", error);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Send message to a WebSocket client
252
+ */
253
+ private sendToClient(ws: WebSocket, message: ConsoleServerMessage): void {
254
+ if (ws.readyState === WebSocket.OPEN) {
255
+ ws.send(JSON.stringify(message));
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Process and display a console message
261
+ */
262
+ private processConsoleMessage(message: ConsoleClientMessage): void {
263
+ // Check if this message type should be filtered
264
+ if (!this.config.filter.includes(message.consoleType)) {
265
+ return;
266
+ }
267
+
268
+ // Format and output the message
269
+ const formatted = this.formatMessage(message);
270
+ this.output(formatted, message.consoleType);
271
+ }
272
+
273
+ /**
274
+ * Format a console message for terminal output
275
+ */
276
+ private formatMessage(message: ConsoleClientMessage): string {
277
+ const parts: string[] = [];
278
+
279
+ // Timestamp
280
+ if (this.config.showTimestamps) {
281
+ const timestamp = this.formatTimestamp(message.timestamp);
282
+ if (this.config.colorize) {
283
+ parts.push(`${ANSI_COLORS.dim}[${timestamp}]${ANSI_COLORS.reset}`);
284
+ } else {
285
+ parts.push(`[${timestamp}]`);
286
+ }
287
+ }
288
+
289
+ // Message type with color
290
+ const typeColor = this.config.colorize ? CONSOLE_TYPE_COLORS[message.consoleType] : "";
291
+ const typeReset = this.config.colorize ? ANSI_COLORS.reset : "";
292
+ const typeLabel = message.consoleType.toUpperCase().padEnd(5);
293
+ parts.push(`${typeColor}${typeLabel}${typeReset}`);
294
+
295
+ // Message arguments
296
+ const formattedArgs = this.formatArgs(message.args, message.consoleType);
297
+ parts.push(formattedArgs);
298
+
299
+ // File:line information
300
+ if (this.config.showFile && message.file) {
301
+ const fileLink = this.formatFileLink(message.file, message.line, message.column);
302
+ parts.push(`\n at ${fileLink}`);
303
+ }
304
+
305
+ // Stack trace for errors
306
+ if (message.stack) {
307
+ const formattedStack = this.formatStackTrace(message.stack);
308
+ parts.push(`\n${formattedStack}`);
309
+ }
310
+
311
+ return parts.join(" ");
312
+ }
313
+
314
+ /**
315
+ * Format timestamp for display
316
+ */
317
+ private formatTimestamp(timestamp: number): string {
318
+ const date = new Date(timestamp);
319
+ const hours = date.getHours().toString().padStart(2, "0");
320
+ const minutes = date.getMinutes().toString().padStart(2, "0");
321
+ const seconds = date.getSeconds().toString().padStart(2, "0");
322
+ const ms = date.getMilliseconds().toString().padStart(3, "0");
323
+ return `${hours}:${minutes}:${seconds}.${ms}`;
324
+ }
325
+
326
+ /**
327
+ * Format console arguments for display
328
+ */
329
+ private formatArgs(args: unknown[], type: ConsoleMessageType): string {
330
+ if (type === "table") {
331
+ return this.formatTable(args);
332
+ }
333
+
334
+ if (type === "trace") {
335
+ // Trace already includes the stack in the args
336
+ return args.map(arg => this.formatValue(arg)).join(" ");
337
+ }
338
+
339
+ return args.map(arg => this.formatValue(arg)).join(" ");
340
+ }
341
+
342
+ /**
343
+ * Format a single value for display
344
+ */
345
+ private formatValue(value: unknown, depth: number = 0): string {
346
+ if (depth > 3) {
347
+ return this.config.colorize ? `${ANSI_COLORS.dim}[...]${ANSI_COLORS.reset}` : "[...]";
348
+ }
349
+
350
+ if (value === null) {
351
+ return this.config.colorize ? `${ANSI_COLORS.gray}null${ANSI_COLORS.reset}` : "null";
352
+ }
353
+
354
+ if (value === undefined) {
355
+ return this.config.colorize ? `${ANSI_COLORS.gray}undefined${ANSI_COLORS.reset}` : "undefined";
356
+ }
357
+
358
+ if (typeof value === "string") {
359
+ // Check if it's a long string
360
+ if (value.length > 200) {
361
+ const truncated = value.substring(0, 200) + "...";
362
+ return this.config.colorize ? `${ANSI_COLORS.green}"${truncated}"${ANSI_COLORS.reset}` : `"${truncated}"`;
363
+ }
364
+ return this.config.colorize ? `${ANSI_COLORS.green}"${value}"${ANSI_COLORS.reset}` : `"${value}"`;
365
+ }
366
+
367
+ if (typeof value === "number") {
368
+ return this.config.colorize ? `${ANSI_COLORS.yellow}${value}${ANSI_COLORS.reset}` : `${value}`;
369
+ }
370
+
371
+ if (typeof value === "boolean") {
372
+ return this.config.colorize ? `${ANSI_COLORS.magenta}${value}${ANSI_COLORS.reset}` : `${value}`;
373
+ }
374
+
375
+ if (value instanceof Error) {
376
+ const errorStr = `${value.name}: ${value.message}`;
377
+ return this.config.colorize ? `${ANSI_COLORS.red}${errorStr}${ANSI_COLORS.reset}` : errorStr;
378
+ }
379
+
380
+ if (Array.isArray(value)) {
381
+ if (value.length === 0) {
382
+ return "[]";
383
+ }
384
+ if (value.length > 10) {
385
+ const items = value.slice(0, 10).map(v => this.formatValue(v, depth + 1));
386
+ return `[${items.join(", ")}, ... ${value.length - 10} more items]`;
387
+ }
388
+ const items = value.map(v => this.formatValue(v, depth + 1));
389
+ return `[${items.join(", ")}]`;
390
+ }
391
+
392
+ if (typeof value === "object") {
393
+ try {
394
+ const entries = Object.entries(value as Record<string, unknown>);
395
+ if (entries.length === 0) {
396
+ return "{}";
397
+ }
398
+ if (entries.length > 5) {
399
+ const shown = entries.slice(0, 5).map(([k, v]) => `${k}: ${this.formatValue(v, depth + 1)}`);
400
+ return `{${shown.join(", ")}, ... ${entries.length - 5} more keys}`;
401
+ }
402
+ const formatted = entries.map(([k, v]) => `${k}: ${this.formatValue(v, depth + 1)}`);
403
+ return `{${formatted.join(", ")}}`;
404
+ } catch {
405
+ return "[Object]";
406
+ }
407
+ }
408
+
409
+ return String(value);
410
+ }
411
+
412
+ /**
413
+ * Format console.table output
414
+ */
415
+ private formatTable(args: unknown[]): string {
416
+ if (args.length === 0) return "";
417
+
418
+ const [data, columns] = args;
419
+
420
+ if (!Array.isArray(data) && typeof data !== "object") {
421
+ return this.formatValue(data);
422
+ }
423
+
424
+ // Simple table formatting
425
+ const entries = Array.isArray(data) ? data : Object.entries(data as object);
426
+
427
+ if (entries.length === 0) {
428
+ return this.config.colorize ? `${ANSI_COLORS.dim}(empty table)${ANSI_COLORS.reset}` : "(empty table)";
429
+ }
430
+
431
+ const lines: string[] = [];
432
+ lines.push(this.config.colorize ? `${ANSI_COLORS.blue}┌─────────${ANSI_COLORS.reset}` : "┌─────────");
433
+
434
+ const maxRows = 10;
435
+ const shown = entries.slice(0, maxRows);
436
+
437
+ for (const entry of shown) {
438
+ const row = this.formatValue(entry, 1);
439
+ lines.push(this.config.colorize ? `${ANSI_COLORS.blue}│${ANSI_COLORS.reset} ${row}` : `│ ${row}`);
440
+ }
441
+
442
+ if (entries.length > maxRows) {
443
+ lines.push(this.config.colorize ? `${ANSI_COLORS.blue}│${ANSI_COLORS.reset} ... ${entries.length - maxRows} more rows` : `│ ... ${entries.length - maxRows} more rows`);
444
+ }
445
+
446
+ lines.push(this.config.colorize ? `${ANSI_COLORS.blue}└─────────${ANSI_COLORS.reset}` : "└─────────");
447
+
448
+ return "\n" + lines.join("\n");
449
+ }
450
+
451
+ /**
452
+ * Format file link for VSCode clickable links
453
+ */
454
+ private formatFileLink(file: string, line?: number, column?: number): string {
455
+ const location = line ? `:${line}${column ? `:${column}` : ""}` : "";
456
+ const link = `${file}${location}`;
457
+
458
+ if (this.config.colorize) {
459
+ return `${ANSI_COLORS.cyan}${link}${ANSI_COLORS.reset}`;
460
+ }
461
+ return link;
462
+ }
463
+
464
+ /**
465
+ * Format stack trace for display
466
+ */
467
+ private formatStackTrace(stack: string): string {
468
+ const lines = stack.split("\n");
469
+ const formatted = lines.map((line, index) => {
470
+ if (index === 0) {
471
+ // First line is usually the error message
472
+ return this.config.colorize
473
+ ? ` ${ANSI_COLORS.red}${line}${ANSI_COLORS.reset}`
474
+ : ` ${line}`;
475
+ }
476
+
477
+ // Try to make file paths clickable
478
+ const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/);
479
+ if (match) {
480
+ const [, fn, file, lineNum, col] = match;
481
+ if (this.config.colorize) {
482
+ return ` at ${fn} (${ANSI_COLORS.cyan}${file}:${lineNum}:${col}${ANSI_COLORS.reset})`;
483
+ }
484
+ return ` at ${fn} (${file}:${lineNum}:${col})`;
485
+ }
486
+
487
+ return this.config.colorize
488
+ ? ` ${ANSI_COLORS.gray}${line}${ANSI_COLORS.reset}`
489
+ : ` ${line}`;
490
+ });
491
+
492
+ return formatted.join("\n");
493
+ }
494
+
495
+ /**
496
+ * Output formatted message to terminal
497
+ */
498
+ private output(formatted: string, type: ConsoleMessageType): void {
499
+ switch (type) {
500
+ case "error":
501
+ console.error(formatted);
502
+ break;
503
+ case "warn":
504
+ console.warn(formatted);
505
+ break;
506
+ default:
507
+ console.log(formatted);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Generate a unique client ID
513
+ */
514
+ private generateClientId(): string {
515
+ return `console_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
516
+ }
517
+
518
+ /**
519
+ * Get the number of connected clients
520
+ */
521
+ getClientCount(): number {
522
+ return this.clients.size;
523
+ }
524
+
525
+ /**
526
+ * Get all connected client IDs
527
+ */
528
+ getClientIds(): string[] {
529
+ return Array.from(this.clients.keys());
530
+ }
531
+
532
+ /**
533
+ * Disconnect all clients
534
+ */
535
+ disconnectAll(): void {
536
+ for (const client of this.clients.values()) {
537
+ if (client.ws.readyState === WebSocket.OPEN) {
538
+ client.ws.close();
539
+ }
540
+ }
541
+
542
+ this.clients.clear();
543
+ this.logger.info("All console clients disconnected");
544
+ }
545
+ }
546
+
547
+ // ============= Factory Function =============
548
+
549
+ /**
550
+ * Create a console stream manager
551
+ */
552
+ export function createConsoleStreamManager(
553
+ devServerPort: number,
554
+ config?: PartialConsoleStreamConfig
555
+ ): ConsoleStreamManager {
556
+ return new ConsoleStreamManager(devServerPort, config);
557
+ }
558
+
559
+ // ============= Utility Functions =============
560
+
561
+ /**
562
+ * Inject console client script into HTML
563
+ */
564
+ export function injectConsoleScript(html: string, port: number): string {
565
+ const script = `
566
+ <script>
567
+ (function() {
568
+ const CONSOLE_PORT = ${port};
569
+ ${CONSOLE_CLIENT_SCRIPT}
570
+ })();
571
+ </script>
572
+ `;
573
+
574
+ // Inject before closing </head> or <body>
575
+ const headMatch = html.match(/<\/head>/i);
576
+ if (headMatch) {
577
+ return html.replace(/<\/head>/i, `${script}</head>`);
578
+ }
579
+
580
+ const bodyMatch = html.match(/<body/i);
581
+ if (bodyMatch) {
582
+ return html.replace(/<body/i, `${script}<body`);
583
+ }
584
+
585
+ // If no head or body, prepend
586
+ return script + html;
587
+ }