@openbuilder/cli 0.31.11

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 (78) hide show
  1. package/README.md +1053 -0
  2. package/bin/openbuilder.js +31 -0
  3. package/dist/chunks/Banner-D4tqKfzA.js +113 -0
  4. package/dist/chunks/Banner-D4tqKfzA.js.map +1 -0
  5. package/dist/chunks/auto-update-Dj3lWPWO.js +350 -0
  6. package/dist/chunks/auto-update-Dj3lWPWO.js.map +1 -0
  7. package/dist/chunks/build-D0qYqIq0.js +116 -0
  8. package/dist/chunks/build-D0qYqIq0.js.map +1 -0
  9. package/dist/chunks/cleanup-qVTsA3tk.js +141 -0
  10. package/dist/chunks/cleanup-qVTsA3tk.js.map +1 -0
  11. package/dist/chunks/cli-error-BjQwvWtK.js +140 -0
  12. package/dist/chunks/cli-error-BjQwvWtK.js.map +1 -0
  13. package/dist/chunks/config-BGP1jZJ4.js +167 -0
  14. package/dist/chunks/config-BGP1jZJ4.js.map +1 -0
  15. package/dist/chunks/config-manager-BkbjtN-H.js +133 -0
  16. package/dist/chunks/config-manager-BkbjtN-H.js.map +1 -0
  17. package/dist/chunks/database-BvAbD4sP.js +68 -0
  18. package/dist/chunks/database-BvAbD4sP.js.map +1 -0
  19. package/dist/chunks/database-setup-BYjIRAmT.js +253 -0
  20. package/dist/chunks/database-setup-BYjIRAmT.js.map +1 -0
  21. package/dist/chunks/exports-ij9sv4UM.js +7793 -0
  22. package/dist/chunks/exports-ij9sv4UM.js.map +1 -0
  23. package/dist/chunks/init-CZoN6soU.js +468 -0
  24. package/dist/chunks/init-CZoN6soU.js.map +1 -0
  25. package/dist/chunks/init-tui-BNzk_7Yx.js +1127 -0
  26. package/dist/chunks/init-tui-BNzk_7Yx.js.map +1 -0
  27. package/dist/chunks/logger-ZpJi7chw.js +38 -0
  28. package/dist/chunks/logger-ZpJi7chw.js.map +1 -0
  29. package/dist/chunks/main-tui-Cq1hLCx-.js +644 -0
  30. package/dist/chunks/main-tui-Cq1hLCx-.js.map +1 -0
  31. package/dist/chunks/manager-CvGX9qqe.js +1161 -0
  32. package/dist/chunks/manager-CvGX9qqe.js.map +1 -0
  33. package/dist/chunks/port-allocator-BRFzgH9b.js +749 -0
  34. package/dist/chunks/port-allocator-BRFzgH9b.js.map +1 -0
  35. package/dist/chunks/process-killer-CaUL7Kpl.js +87 -0
  36. package/dist/chunks/process-killer-CaUL7Kpl.js.map +1 -0
  37. package/dist/chunks/prompts-1QbE_bRr.js +128 -0
  38. package/dist/chunks/prompts-1QbE_bRr.js.map +1 -0
  39. package/dist/chunks/repo-cloner-CpOQjFSo.js +219 -0
  40. package/dist/chunks/repo-cloner-CpOQjFSo.js.map +1 -0
  41. package/dist/chunks/repo-detector-B_oj696o.js +66 -0
  42. package/dist/chunks/repo-detector-B_oj696o.js.map +1 -0
  43. package/dist/chunks/run-D23hg4xy.js +630 -0
  44. package/dist/chunks/run-D23hg4xy.js.map +1 -0
  45. package/dist/chunks/runner-logger-instance-nDWv2h2T.js +899 -0
  46. package/dist/chunks/runner-logger-instance-nDWv2h2T.js.map +1 -0
  47. package/dist/chunks/spinner-BJL9zWAJ.js +53 -0
  48. package/dist/chunks/spinner-BJL9zWAJ.js.map +1 -0
  49. package/dist/chunks/start-BygPCbvw.js +1708 -0
  50. package/dist/chunks/start-BygPCbvw.js.map +1 -0
  51. package/dist/chunks/start-traditional-uoLZXdxm.js +255 -0
  52. package/dist/chunks/start-traditional-uoLZXdxm.js.map +1 -0
  53. package/dist/chunks/status-cS8YwtUx.js +97 -0
  54. package/dist/chunks/status-cS8YwtUx.js.map +1 -0
  55. package/dist/chunks/theme-DhorI2Hb.js +44 -0
  56. package/dist/chunks/theme-DhorI2Hb.js.map +1 -0
  57. package/dist/chunks/upgrade-CT6w0lKp.js +323 -0
  58. package/dist/chunks/upgrade-CT6w0lKp.js.map +1 -0
  59. package/dist/chunks/useBuildState-CdBSu9y_.js +331 -0
  60. package/dist/chunks/useBuildState-CdBSu9y_.js.map +1 -0
  61. package/dist/cli/index.js +694 -0
  62. package/dist/cli/index.js.map +1 -0
  63. package/dist/index.js +14358 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/instrument.js +64226 -0
  66. package/dist/instrument.js.map +1 -0
  67. package/dist/templates.json +295 -0
  68. package/package.json +98 -0
  69. package/scripts/install-vendor-deps.js +34 -0
  70. package/scripts/install-vendor.js +167 -0
  71. package/scripts/prepare-release.js +71 -0
  72. package/templates/config.template.json +18 -0
  73. package/templates.json +295 -0
  74. package/vendor/ai-sdk-provider-claude-code-LOCAL.tgz +0 -0
  75. package/vendor/sentry-core-LOCAL.tgz +0 -0
  76. package/vendor/sentry-nextjs-LOCAL.tgz +0 -0
  77. package/vendor/sentry-node-LOCAL.tgz +0 -0
  78. package/vendor/sentry-node-core-LOCAL.tgz +0 -0
@@ -0,0 +1,1708 @@
1
+ // OpenBuilder CLI - Built with Rollup
2
+ import { existsSync, mkdirSync, createWriteStream, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { Box, Text, useApp, useStdout, useInput, render } from 'ink';
5
+ import React, { useState, useMemo, useEffect } from 'react';
6
+ import * as p from '@clack/prompts';
7
+ import pc from 'picocolors';
8
+ import { c as configManager } from './config-manager-BkbjtN-H.js';
9
+ import { i as isInsideMonorepo } from './repo-detector-B_oj696o.js';
10
+ import { killProcessOnPort, killProcessTree } from './process-killer-CaUL7Kpl.js';
11
+ import { e as errors, C as CLIError } from './cli-error-BjQwvWtK.js';
12
+ import { spawn } from 'node:child_process';
13
+ import EventEmitter from 'node:events';
14
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
+ import TextInput from 'ink-text-input';
16
+ import { exec } from 'child_process';
17
+ import { platform } from 'os';
18
+ import { u as useBuildState, B as BuildPanel } from './useBuildState-CdBSu9y_.js';
19
+ import 'chalk';
20
+ import { i as initRunnerLogger, s as setFileLoggerTuiMode } from './runner-logger-instance-nDWv2h2T.js';
21
+ import 'conf';
22
+ import 'node:os';
23
+ import 'node:fs/promises';
24
+ import 'node:util';
25
+ import './theme-DhorI2Hb.js';
26
+
27
+ /**
28
+ * ServiceManager - Manages lifecycle and state of Web App, Broker, and Runner
29
+ * Provides state updates for TUI dashboard
30
+ */
31
+ class ServiceManager extends EventEmitter {
32
+ constructor() {
33
+ super();
34
+ this.services = new Map();
35
+ }
36
+ /**
37
+ * Register a service configuration
38
+ */
39
+ register(config) {
40
+ this.services.set(config.name, {
41
+ config,
42
+ state: {
43
+ name: config.name,
44
+ displayName: config.displayName,
45
+ status: 'stopped',
46
+ port: config.port,
47
+ uptime: 0,
48
+ },
49
+ });
50
+ }
51
+ /**
52
+ * Start a specific service
53
+ */
54
+ async start(name) {
55
+ const service = this.services.get(name);
56
+ if (!service) {
57
+ throw new Error(`Service ${name} not registered`);
58
+ }
59
+ if (service.process && !service.process.killed) {
60
+ throw new Error(`Service ${name} is already running`);
61
+ }
62
+ // Update state to starting
63
+ service.state.status = 'starting';
64
+ this.emit('service:status-change', name, 'starting');
65
+ try {
66
+ // Spawn the process
67
+ const proc = spawn(service.config.command, service.config.args, {
68
+ cwd: service.config.cwd,
69
+ stdio: ['ignore', 'pipe', 'pipe'],
70
+ shell: true,
71
+ env: {
72
+ ...process.env,
73
+ ...service.config.env,
74
+ },
75
+ });
76
+ service.process = proc;
77
+ service.state.pid = proc.pid;
78
+ service.startTime = Date.now();
79
+ // Handle stdout - ONLY emit events, NO console.log
80
+ proc.stdout?.on('data', (data) => {
81
+ const output = data.toString();
82
+ service.state.lastOutput = output.trim();
83
+ // Emit event - TUI will handle display
84
+ this.emit('service:output', name, output, 'stdout');
85
+ // Detect when service is ready
86
+ if (this.isServiceReady(name, output)) {
87
+ service.state.status = 'running';
88
+ this.emit('service:status-change', name, 'running');
89
+ }
90
+ });
91
+ // Handle stderr - ONLY emit events, NO console.log
92
+ proc.stderr?.on('data', (data) => {
93
+ const output = data.toString();
94
+ // Emit event - TUI will handle display
95
+ this.emit('service:output', name, output, 'stderr');
96
+ // Check for errors in stderr
97
+ if (output.toLowerCase().includes('error') && !output.includes('warn')) {
98
+ service.state.error = output.trim();
99
+ }
100
+ });
101
+ // Handle process exit
102
+ proc.on('exit', (code, signal) => {
103
+ if (code !== 0 && code !== null && code !== 130 && code !== 143) {
104
+ // Abnormal exit
105
+ service.state.status = 'error';
106
+ service.state.error = `Exited with code ${code}`;
107
+ this.emit('service:status-change', name, 'error');
108
+ this.emit('service:error', name, new Error(`Process exited with code ${code}`));
109
+ }
110
+ else {
111
+ // Normal exit
112
+ service.state.status = 'stopped';
113
+ this.emit('service:status-change', name, 'stopped');
114
+ }
115
+ service.process = undefined;
116
+ service.state.pid = undefined;
117
+ service.startTime = undefined;
118
+ });
119
+ proc.on('error', (error) => {
120
+ service.state.status = 'error';
121
+ service.state.error = error.message;
122
+ this.emit('service:status-change', name, 'error');
123
+ this.emit('service:error', name, error);
124
+ });
125
+ }
126
+ catch (error) {
127
+ service.state.status = 'error';
128
+ service.state.error = error instanceof Error ? error.message : 'Unknown error';
129
+ this.emit('service:status-change', name, 'error');
130
+ this.emit('service:error', name, error instanceof Error ? error : new Error(String(error)));
131
+ throw error;
132
+ }
133
+ }
134
+ /**
135
+ * Start all registered services in sequence
136
+ */
137
+ async startAll(delayBetween = 2000) {
138
+ const services = Array.from(this.services.keys());
139
+ for (const name of services) {
140
+ await this.start(name);
141
+ if (delayBetween > 0) {
142
+ await new Promise(resolve => setTimeout(resolve, delayBetween));
143
+ }
144
+ }
145
+ // Start periodic updates for uptime/stats
146
+ this.startUpdates();
147
+ this.emit('all:started');
148
+ }
149
+ /**
150
+ * Stop a specific service
151
+ */
152
+ async stop(name, signal = 'SIGTERM') {
153
+ const service = this.services.get(name);
154
+ if (!service) {
155
+ return;
156
+ }
157
+ const proc = service.process;
158
+ const port = service.config.port;
159
+ const pid = proc?.pid;
160
+ // If no process, just try to kill by port as cleanup
161
+ if (!proc) {
162
+ if (port) {
163
+ await killProcessOnPort(port);
164
+ }
165
+ return;
166
+ }
167
+ return new Promise(async (resolve) => {
168
+ // Set timeout for force kill
169
+ const timeout = setTimeout(async () => {
170
+ // Try killing the process tree first
171
+ if (pid) {
172
+ await killProcessTree(pid, 'SIGKILL');
173
+ }
174
+ // Also kill by port as final fallback
175
+ if (port) {
176
+ await killProcessOnPort(port);
177
+ }
178
+ resolve();
179
+ }, 2000);
180
+ proc.once('exit', () => {
181
+ clearTimeout(timeout);
182
+ resolve();
183
+ });
184
+ // First try graceful SIGTERM via process tree
185
+ if (pid) {
186
+ await killProcessTree(pid, signal);
187
+ }
188
+ else {
189
+ proc.kill(signal);
190
+ }
191
+ });
192
+ }
193
+ /**
194
+ * Stop all running services
195
+ */
196
+ async stopAll() {
197
+ this.stopUpdates();
198
+ const services = Array.from(this.services.keys()).reverse(); // Stop in reverse order
199
+ // Collect all ports for final cleanup
200
+ const ports = [];
201
+ for (const name of services) {
202
+ const service = this.services.get(name);
203
+ if (service?.config.port) {
204
+ ports.push(service.config.port);
205
+ }
206
+ }
207
+ // Stop each service
208
+ for (const name of services) {
209
+ await this.stop(name);
210
+ }
211
+ // Final port cleanup - ensure no zombie processes remain
212
+ // Small delay to let processes actually terminate
213
+ await new Promise(resolve => setTimeout(resolve, 500));
214
+ for (const port of ports) {
215
+ await killProcessOnPort(port);
216
+ }
217
+ this.emit('all:stopped');
218
+ }
219
+ /**
220
+ * Get current state of a service
221
+ */
222
+ getState(name) {
223
+ return this.services.get(name)?.state;
224
+ }
225
+ /**
226
+ * Get current state of all services
227
+ */
228
+ getAllStates() {
229
+ return Array.from(this.services.values()).map(s => s.state);
230
+ }
231
+ /**
232
+ * Check if a service is running
233
+ */
234
+ isRunning(name) {
235
+ return this.services.get(name)?.state.status === 'running';
236
+ }
237
+ /**
238
+ * Check if all services are running
239
+ */
240
+ areAllRunning() {
241
+ return Array.from(this.services.values()).every(s => s.state.status === 'running');
242
+ }
243
+ /**
244
+ * Restart a service
245
+ */
246
+ async restart(name) {
247
+ await this.stop(name);
248
+ await new Promise(resolve => setTimeout(resolve, 1000));
249
+ await this.start(name);
250
+ }
251
+ /**
252
+ * Restart all services
253
+ */
254
+ async restartAll() {
255
+ await this.stopAll();
256
+ await new Promise(resolve => setTimeout(resolve, 1000));
257
+ await this.startAll();
258
+ }
259
+ /**
260
+ * Start periodic state updates (uptime, memory, cpu)
261
+ */
262
+ startUpdates() {
263
+ this.updateInterval = setInterval(() => {
264
+ for (const [name, service] of this.services) {
265
+ if (service.startTime) {
266
+ service.state.uptime = Date.now() - service.startTime;
267
+ }
268
+ // TODO: Add memory/CPU monitoring using ps or similar
269
+ // For now, just update uptime
270
+ }
271
+ }, 1000); // Update every second
272
+ }
273
+ /**
274
+ * Stop periodic updates
275
+ */
276
+ stopUpdates() {
277
+ if (this.updateInterval) {
278
+ clearInterval(this.updateInterval);
279
+ this.updateInterval = undefined;
280
+ }
281
+ }
282
+ /**
283
+ * Detect if service is ready based on output
284
+ */
285
+ isServiceReady(name, output) {
286
+ const lowerOutput = output.toLowerCase();
287
+ switch (name) {
288
+ case 'web':
289
+ return lowerOutput.includes('ready') || lowerOutput.includes('started server');
290
+ case 'broker':
291
+ return lowerOutput.includes('listening') || lowerOutput.includes('ready');
292
+ case 'runner':
293
+ return lowerOutput.includes('connected') || lowerOutput.includes('ready');
294
+ default:
295
+ return false;
296
+ }
297
+ }
298
+ /**
299
+ * Create a Cloudflare tunnel for a service
300
+ */
301
+ async createTunnel(name) {
302
+ const service = this.services.get(name);
303
+ if (!service || !service.state.port) {
304
+ throw new Error(`Service ${name} not found or has no port`);
305
+ }
306
+ // Update state to creating
307
+ service.state.tunnelStatus = 'creating';
308
+ this.emit('service:tunnel-change', name, null, 'creating');
309
+ try {
310
+ // Import tunnel manager
311
+ const { tunnelManager } = await import('./manager-CvGX9qqe.js');
312
+ // Enable silent mode for TUI
313
+ tunnelManager.setSilent(true);
314
+ // Create tunnel
315
+ const tunnelUrl = await tunnelManager.createTunnel(service.state.port);
316
+ // Update state
317
+ service.state.tunnelUrl = tunnelUrl;
318
+ service.state.tunnelStatus = 'active';
319
+ this.emit('service:tunnel-change', name, tunnelUrl, 'active');
320
+ return tunnelUrl;
321
+ }
322
+ catch (error) {
323
+ service.state.tunnelStatus = 'failed';
324
+ service.state.error = error instanceof Error ? error.message : 'Tunnel creation failed';
325
+ this.emit('service:tunnel-change', name, null, 'failed');
326
+ return null;
327
+ }
328
+ }
329
+ /**
330
+ * Close tunnel for a service
331
+ */
332
+ async closeTunnel(name) {
333
+ const service = this.services.get(name);
334
+ if (!service || !service.state.port) {
335
+ return;
336
+ }
337
+ // Update state immediately
338
+ service.state.tunnelUrl = undefined;
339
+ service.state.tunnelStatus = undefined;
340
+ this.emit('service:tunnel-change', name, null, 'active');
341
+ try {
342
+ const { tunnelManager } = await import('./manager-CvGX9qqe.js');
343
+ await tunnelManager.closeTunnel(service.state.port);
344
+ }
345
+ catch (error) {
346
+ // Best effort
347
+ }
348
+ }
349
+ /**
350
+ * Get tunnel URL for a service
351
+ */
352
+ getTunnelUrl(name) {
353
+ return this.services.get(name)?.state.tunnelUrl || null;
354
+ }
355
+ /**
356
+ * Cleanup and remove all listeners
357
+ */
358
+ destroy() {
359
+ this.stopUpdates();
360
+ this.removeAllListeners();
361
+ }
362
+ }
363
+
364
+ // Theme colors matching init TUI
365
+ const colors = {
366
+ cyan: '#06b6d4',
367
+ brightPurple: '#c084fc',
368
+ };
369
+ /**
370
+ * ASCII art banner component - centered with cyan/purple gradient
371
+ */
372
+ function Banner() {
373
+ const lines = [
374
+ { open: ' ██████╗ ██████╗ ███████╗███╗ ██╗', builder: '██████╗ ██╗ ██╗██╗██╗ ██████╗ ███████╗██████╗ ' },
375
+ { open: '██╔═══██╗██╔══██╗██╔════╝████╗ ██║', builder: '██╔══██╗██║ ██║██║██║ ██╔══██╗██╔════╝██╔══██╗' },
376
+ { open: '██║ ██║██████╔╝█████╗ ██╔██╗ ██║', builder: '██████╔╝██║ ██║██║██║ ██║ ██║█████╗ ██████╔╝' },
377
+ { open: '██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║', builder: '██╔══██╗██║ ██║██║██║ ██║ ██║██╔══╝ ██╔══██╗' },
378
+ { open: '╚██████╔╝██║ ███████╗██║ ╚████║', builder: '██████╔╝╚██████╔╝██║███████╗██████╔╝███████╗██║ ██║' },
379
+ { open: ' ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝', builder: '╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚══════╝╚═╝ ╚═╝' },
380
+ ];
381
+ return (jsx(Box, { flexDirection: "column", alignItems: "center", marginTop: 2, children: lines.map((line, index) => (jsxs(Box, { children: [jsx(Text, { color: colors.cyan, children: line.open }), jsx(Text, { color: colors.brightPurple, children: line.builder })] }, index))) }));
382
+ }
383
+
384
+ // Base colors that don't change with theme
385
+ const baseColors = {
386
+ success: '#22c55e',
387
+ error: '#ef4444',
388
+ warning: '#f59e0b',
389
+ cyan: '#06b6d4',
390
+ white: '#ffffff',
391
+ gray: '#6b7280',
392
+ dimGray: '#4b5563'};
393
+ const symbols = {
394
+ filledDot: '●',
395
+ hollowDot: '○',
396
+ check: '✓',
397
+ cross: '✗'};
398
+ const THEMES = {
399
+ sentry: {
400
+ name: 'sentry',
401
+ label: 'Sentry',
402
+ description: 'Purple-pink gradient',
403
+ colors: {
404
+ primary: '#a855f7',
405
+ secondary: '#ec4899',
406
+ accent: '#c084fc',
407
+ muted: '#7c3aed',
408
+ },
409
+ },
410
+ ocean: {
411
+ name: 'ocean',
412
+ label: 'Ocean',
413
+ description: 'Cool blue & teal',
414
+ colors: {
415
+ primary: '#3b82f6',
416
+ secondary: '#22d3ee',
417
+ accent: '#60a5fa',
418
+ muted: '#2563eb',
419
+ },
420
+ },
421
+ ember: {
422
+ name: 'ember',
423
+ label: 'Ember',
424
+ description: 'Warm orange & red',
425
+ colors: {
426
+ primary: '#f97316',
427
+ secondary: '#ef4444',
428
+ accent: '#fb923c',
429
+ muted: '#ea580c',
430
+ },
431
+ },
432
+ forest: {
433
+ name: 'forest',
434
+ label: 'Forest',
435
+ description: 'Green & earth tones',
436
+ colors: {
437
+ primary: '#10b981',
438
+ secondary: '#84cc16',
439
+ accent: '#34d399',
440
+ muted: '#059669',
441
+ },
442
+ },
443
+ noir: {
444
+ name: 'noir',
445
+ label: 'Noir',
446
+ description: 'Monochrome dark',
447
+ colors: {
448
+ primary: '#ffffff',
449
+ secondary: '#a1a1aa',
450
+ accent: '#e4e4e7',
451
+ muted: '#71717a',
452
+ },
453
+ },
454
+ };
455
+ const THEME_ORDER = ['sentry', 'ocean', 'ember', 'forest', 'noir'];
456
+ function formatTime(date) {
457
+ return date.toLocaleTimeString('en-US', {
458
+ hour12: false,
459
+ hour: '2-digit',
460
+ minute: '2-digit',
461
+ second: '2-digit',
462
+ });
463
+ }
464
+ function getLogLevel(message, stream) {
465
+ if (stream === 'stderr')
466
+ return 'warn';
467
+ const lower = message.toLowerCase();
468
+ if (lower.includes('error') || lower.includes('failed') || lower.includes('exception'))
469
+ return 'error';
470
+ if (lower.includes('warn') || lower.includes('warning'))
471
+ return 'warn';
472
+ if (lower.includes('success') || lower.includes('ready') || lower.includes('started') || lower.includes('connected') || lower.includes('✅') || lower.includes('✓'))
473
+ return 'success';
474
+ return 'info';
475
+ }
476
+ /**
477
+ * Determines if a log message is internal/debug and should be hidden by default.
478
+ * These are implementation details that aren't useful for end users.
479
+ */
480
+ function isInternalLog(message) {
481
+ const internalPatterns = [
482
+ // Internal broadcasting/event system
483
+ /Broadcasting/i,
484
+ /Event emitted/i,
485
+ /📡.*Broadcasting/,
486
+ // Session/ID tracking details
487
+ /sessionId=/,
488
+ /todoIndex=/,
489
+ /buildId=/,
490
+ /commandId=/,
491
+ // Internal processor notes
492
+ /NOTE:.*DB writes/,
493
+ /NOTE:.*HTTP/,
494
+ /Registering build.*for WebSocket/,
495
+ // Verbose implementation details
496
+ /from database\)/,
497
+ /immutable/,
498
+ /\(waiting for runner/,
499
+ // Duplicate/skip messages
500
+ /Skipping duplicate/,
501
+ // Internal state
502
+ /SSE stream closed/,
503
+ /persistent processor continues/,
504
+ // Cost/optimization notes (internal)
505
+ /Cost savings:/,
506
+ ];
507
+ return internalPatterns.some(pattern => pattern.test(message));
508
+ }
509
+ /**
510
+ * Transforms a raw log message into a user-friendly format.
511
+ * Extracts meaningful information and presents it clearly.
512
+ */
513
+ function transformLogMessage(message) {
514
+ // NEW FORMAT: Server now logs "🔧 Edit: /path/to/file.ts" or "🔧 Run: npm install"
515
+ // Match: 🔧 Action: details
516
+ const newToolFormat = message.match(/🔧\s*(Read|Edit|Write|Run|Find|Search|Fetch|Update tasks):\s*(.+)/i);
517
+ if (newToolFormat) {
518
+ const action = newToolFormat[1];
519
+ const detail = newToolFormat[2].trim();
520
+ return { display: `${action}: ${detail}`, isUserFacing: true, toolAction: action };
521
+ }
522
+ // Also match bare tool names for backwards compat: "🔧 Read" (no colon/details)
523
+ const bareToolMatch = message.match(/^🔧\s*(Read|Edit|Write|Bash|Glob|Grep|WebFetch|TodoWrite)$/i);
524
+ if (bareToolMatch) {
525
+ return { display: bareToolMatch[1], isUserFacing: true, toolAction: bareToolMatch[1] };
526
+ }
527
+ // OLD FORMAT: Tool calls with parentheses like "🔧 Read (todoIndex=..."
528
+ // Try to extract any useful info
529
+ const oldToolFormat = message.match(/🔧\s*(Read|Edit|Write|Bash|Glob|Grep)\s*\(/i);
530
+ if (oldToolFormat) {
531
+ const toolName = oldToolFormat[1];
532
+ // Try to extract file path from the message
533
+ const pathMatch = message.match(/(?:path|file)[:=]\s*["']?([^"'\s,)]+)/i);
534
+ const cmdMatch = message.match(/(?:command|cmd)[:=]\s*["']?([^"'\n]+)/i);
535
+ if (toolName.toLowerCase() === 'bash' && cmdMatch) {
536
+ const cmd = cmdMatch[1].trim().substring(0, 50);
537
+ return { display: `Run: ${cmd}${cmdMatch[1].length > 50 ? '...' : ''}`, isUserFacing: true, toolAction: 'Run' };
538
+ }
539
+ else if (pathMatch) {
540
+ return { display: `${toolName}: ${pathMatch[1]}`, isUserFacing: true, toolAction: toolName };
541
+ }
542
+ // No details found, just show the tool name
543
+ return { display: toolName, isUserFacing: true, toolAction: toolName };
544
+ }
545
+ // Template/framework selection - always show
546
+ const templateMatch = message.match(/Template (?:selected|from tag):\s*(.+)/i);
547
+ if (templateMatch) {
548
+ return { display: `Using template: ${templateMatch[1]}`, isUserFacing: true };
549
+ }
550
+ const frameworkMatch = message.match(/Framework.*?:\s*(\w+)/i);
551
+ if (frameworkMatch && !message.includes('emit')) {
552
+ return { display: `Framework: ${frameworkMatch[1]}`, isUserFacing: true };
553
+ }
554
+ // Build start
555
+ if (message.includes('start event received') || message.includes('build-started')) {
556
+ return { display: 'Build started', isUserFacing: true };
557
+ }
558
+ // Build complete
559
+ if (message.includes('marked complete') || message.includes('Build complete')) {
560
+ return { display: 'Build complete', isUserFacing: true };
561
+ }
562
+ // Agent selection - clean it up
563
+ const agentMatch = message.match(/(?:Using agent|Agent).*?:\s*(\S+)/i);
564
+ if (agentMatch && !message.includes('NOTE:')) {
565
+ return { display: `Agent: ${agentMatch[1].replace(/[()]/g, '')}`, isUserFacing: true };
566
+ }
567
+ // Generic success messages
568
+ if (message.includes('✅') && !isInternalLog(message)) {
569
+ // Clean up the message
570
+ const cleaned = message.replace(/\[[\w-]+\]\s*/g, '').replace(/✅\s*/, '').trim();
571
+ if (cleaned.length > 10) {
572
+ return { display: cleaned, isUserFacing: true };
573
+ }
574
+ }
575
+ // Default: not user-facing if it's internal
576
+ return { display: message, isUserFacing: !isInternalLog(message) };
577
+ }
578
+ function parseLogMessage(message) {
579
+ let tag;
580
+ let emoji;
581
+ let toolName;
582
+ let content = message;
583
+ const tagMatch = content.match(/^\[([^\]]+)\]\s*/);
584
+ if (tagMatch) {
585
+ tag = tagMatch[1];
586
+ content = content.substring(tagMatch[0].length);
587
+ }
588
+ const emojiMatch = content.match(/^([\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|✅|✓|📜|📤|🔧|📡|⚠️|❌)\s*/u);
589
+ if (emojiMatch) {
590
+ emoji = emojiMatch[1];
591
+ content = content.substring(emojiMatch[0].length);
592
+ }
593
+ const toolMatch = content.match(/^tool-input-available:\s*(\w+)/i);
594
+ if (toolMatch) {
595
+ toolName = toolMatch[1];
596
+ emoji = '🔧';
597
+ }
598
+ // Also detect tool calls in format: "🔧 ToolName (..."
599
+ const toolCallMatch = content.match(/^(Read|Edit|Write|Bash|Glob|Grep|TodoWrite)\s*\(/i);
600
+ if (toolCallMatch) {
601
+ toolName = toolCallMatch[1];
602
+ emoji = '🔧';
603
+ }
604
+ return { tag, emoji, toolName, content: content.trim(), isInternal: isInternalLog(message) };
605
+ }
606
+ function openBrowser(url) {
607
+ const os = platform();
608
+ let command;
609
+ if (os === 'darwin') {
610
+ command = `open "${url}"`;
611
+ }
612
+ else if (os === 'win32') {
613
+ command = `start "" "${url}"`;
614
+ }
615
+ else {
616
+ command = `xdg-open "${url}"`;
617
+ }
618
+ exec(command, () => { });
619
+ }
620
+ function Dashboard({ serviceManager, apiUrl, webPort, logFilePath }) {
621
+ const { exit } = useApp();
622
+ const { stdout } = useStdout();
623
+ const [view, setView] = useState('dashboard');
624
+ const [services, setServices] = useState([]);
625
+ const [logs, setLogs] = useState([]);
626
+ const [isShuttingDown, setIsShuttingDown] = useState(false);
627
+ const [serviceFilter, setServiceFilter] = useState(null);
628
+ const [scrollOffset, setScrollOffset] = useState(0);
629
+ const [autoScroll, setAutoScroll] = useState(true);
630
+ const [searchMode, setSearchMode] = useState(false);
631
+ const [searchQuery, setSearchQuery] = useState('');
632
+ const [isVerbose, setIsVerbose] = useState(false);
633
+ const [logIdCounter, setLogIdCounter] = useState(0);
634
+ const [filterMode, setFilterMode] = useState('all');
635
+ const [fullLogSearchMode, setFullLogSearchMode] = useState(false);
636
+ const [fullLogSearchQuery, setFullLogSearchQuery] = useState('');
637
+ const [fullLogScrollOffset, setFullLogScrollOffset] = useState(0);
638
+ // Theme state - load from config, default to 'sentry'
639
+ const savedTheme = configManager.get('ui')?.theme;
640
+ const [selectedTheme, setSelectedTheme] = useState(savedTheme || 'sentry');
641
+ const [previewTheme, setPreviewTheme] = useState(null);
642
+ const [themeBeforePreview, setThemeBeforePreview] = useState(null);
643
+ // Build state - tracks active builds and todos from the RunnerLogger
644
+ const [buildState] = useBuildState();
645
+ // Get current theme colors - use preview theme if active, otherwise selected theme
646
+ const activeThemeName = previewTheme || selectedTheme;
647
+ const theme = THEMES[activeThemeName];
648
+ const themeColors = useMemo(() => ({
649
+ primary: theme.colors.primary,
650
+ secondary: theme.colors.secondary,
651
+ accent: theme.colors.accent,
652
+ muted: theme.colors.muted,
653
+ // Map to semantic colors
654
+ highlight: theme.colors.primary,
655
+ border: theme.colors.muted,
656
+ text: baseColors.white,
657
+ textDim: baseColors.gray,
658
+ textMuted: baseColors.dimGray,
659
+ }), [theme]);
660
+ const terminalHeight = stdout?.rows || 40;
661
+ const terminalWidth = stdout?.columns || 80;
662
+ const bannerHeight = 7;
663
+ const headerHeight = 3;
664
+ const statusBarHeight = 3;
665
+ const contentHeight = Math.max(1, terminalHeight - bannerHeight - headerHeight - statusBarHeight);
666
+ // Show build panel only when there's an active build
667
+ const showBuildPanel = buildState.currentBuild !== null;
668
+ // 20/80 split when build panel is shown, otherwise full width
669
+ const buildPanelWidth = Math.floor(terminalWidth * 0.2);
670
+ const logPanelWidth = showBuildPanel ? terminalWidth - buildPanelWidth : terminalWidth;
671
+ const allServicesRunning = useMemo(() => {
672
+ return services.length > 0 && services.every(s => s.status === 'running');
673
+ }, [services]);
674
+ useEffect(() => {
675
+ const handleStatusChange = () => {
676
+ setServices(serviceManager.getAllStates());
677
+ };
678
+ setServices(serviceManager.getAllStates());
679
+ serviceManager.on('service:status-change', handleStatusChange);
680
+ return () => {
681
+ serviceManager.off('service:status-change', handleStatusChange);
682
+ };
683
+ }, [serviceManager]);
684
+ useEffect(() => {
685
+ const handleServiceOutput = (name, output, stream) => {
686
+ const lines = output.split('\n').filter(line => line.trim());
687
+ setLogIdCounter(prev => {
688
+ const newLogs = lines.map((line, idx) => {
689
+ const trimmed = line.trim();
690
+ const parsed = parseLogMessage(trimmed);
691
+ const transformed = transformLogMessage(trimmed);
692
+ return {
693
+ id: `${Date.now()}-${prev + idx}`,
694
+ timestamp: new Date(),
695
+ service: name,
696
+ message: trimmed,
697
+ stream,
698
+ level: getLogLevel(trimmed, stream),
699
+ tag: parsed.tag,
700
+ emoji: parsed.emoji,
701
+ toolName: parsed.toolName,
702
+ content: parsed.content,
703
+ isInternal: parsed.isInternal,
704
+ displayMessage: transformed.isUserFacing ? transformed.display : undefined,
705
+ };
706
+ });
707
+ if (newLogs.length > 0) {
708
+ setLogs(prevLogs => {
709
+ const combined = [...prevLogs, ...newLogs];
710
+ return combined.slice(-1e4);
711
+ });
712
+ }
713
+ return prev + lines.length;
714
+ });
715
+ };
716
+ serviceManager.on('service:output', handleServiceOutput);
717
+ return () => {
718
+ serviceManager.off('service:output', handleServiceOutput);
719
+ };
720
+ }, [serviceManager]);
721
+ const filteredLogs = useMemo(() => {
722
+ let filtered = logs;
723
+ // By default, hide internal/debug logs unless verbose mode is on
724
+ if (!isVerbose) {
725
+ filtered = filtered.filter(log => !log.isInternal &&
726
+ !log.message.toLowerCase().includes('debug') &&
727
+ !log.message.toLowerCase().includes('trace'));
728
+ }
729
+ if (serviceFilter) {
730
+ filtered = filtered.filter(log => log.service === serviceFilter);
731
+ }
732
+ if (searchQuery.trim()) {
733
+ const query = searchQuery.toLowerCase();
734
+ filtered = filtered.filter(log => log.message.toLowerCase().includes(query) ||
735
+ log.service.toLowerCase().includes(query));
736
+ }
737
+ return filtered;
738
+ }, [logs, serviceFilter, searchQuery, isVerbose]);
739
+ const fullLogFilteredLogs = useMemo(() => {
740
+ let filtered = logs;
741
+ if (filterMode === 'errors') {
742
+ filtered = filtered.filter(log => log.level === 'error' || log.level === 'warn');
743
+ }
744
+ else if (filterMode === 'tools') {
745
+ filtered = filtered.filter(log => log.toolName || log.message.includes('tool-input'));
746
+ }
747
+ else if (filterMode === 'verbose') ;
748
+ else {
749
+ // Default 'all' mode - hide internal logs
750
+ filtered = filtered.filter(log => !log.isInternal &&
751
+ !log.message.toLowerCase().includes('debug') &&
752
+ !log.message.toLowerCase().includes('trace'));
753
+ }
754
+ if (fullLogSearchQuery.trim()) {
755
+ const query = fullLogSearchQuery.toLowerCase();
756
+ filtered = filtered.filter(log => log.message.toLowerCase().includes(query) ||
757
+ log.service.toLowerCase().includes(query) ||
758
+ (log.tag && log.tag.toLowerCase().includes(query)) ||
759
+ (log.toolName && log.toolName.toLowerCase().includes(query)));
760
+ }
761
+ return filtered;
762
+ }, [logs, filterMode, fullLogSearchQuery]);
763
+ const visibleLines = Math.max(1, contentHeight - 3);
764
+ const fullLogVisibleLines = Math.max(1, terminalHeight - 6);
765
+ useEffect(() => {
766
+ if (autoScroll && filteredLogs.length > 0) {
767
+ const maxScroll = Math.max(0, filteredLogs.length - visibleLines);
768
+ setScrollOffset(maxScroll);
769
+ }
770
+ }, [filteredLogs.length, autoScroll, visibleLines]);
771
+ useEffect(() => {
772
+ if (view === 'fullLog') {
773
+ const maxScroll = Math.max(0, fullLogFilteredLogs.length - fullLogVisibleLines);
774
+ setFullLogScrollOffset(maxScroll);
775
+ }
776
+ }, [fullLogFilteredLogs.length, view, fullLogVisibleLines]);
777
+ const displayedLogs = useMemo(() => {
778
+ return filteredLogs.slice(scrollOffset, scrollOffset + visibleLines);
779
+ }, [filteredLogs, scrollOffset, visibleLines]);
780
+ const fullLogDisplayedLogs = useMemo(() => {
781
+ return fullLogFilteredLogs.slice(fullLogScrollOffset, fullLogScrollOffset + fullLogVisibleLines);
782
+ }, [fullLogFilteredLogs, fullLogScrollOffset, fullLogVisibleLines]);
783
+ // Save theme to config and update state
784
+ const saveTheme = (newTheme) => {
785
+ setSelectedTheme(newTheme);
786
+ setPreviewTheme(null);
787
+ setThemeBeforePreview(null);
788
+ // Persist to config
789
+ configManager.set('ui', { theme: newTheme });
790
+ };
791
+ // Open theme selector
792
+ const openThemeSelector = () => {
793
+ setThemeBeforePreview(selectedTheme);
794
+ setPreviewTheme(selectedTheme);
795
+ setView('themeSelector');
796
+ };
797
+ // Cancel theme selection (revert to previous)
798
+ const cancelThemeSelection = () => {
799
+ setPreviewTheme(null);
800
+ setThemeBeforePreview(null);
801
+ setView('dashboard');
802
+ };
803
+ // Confirm theme selection
804
+ const confirmThemeSelection = () => {
805
+ if (previewTheme) {
806
+ saveTheme(previewTheme);
807
+ }
808
+ setView('dashboard');
809
+ };
810
+ useInput((input, key) => {
811
+ if (isShuttingDown)
812
+ return;
813
+ // Theme selector view
814
+ if (view === 'themeSelector') {
815
+ if (key.escape) {
816
+ cancelThemeSelection();
817
+ return;
818
+ }
819
+ if (key.return) {
820
+ confirmThemeSelection();
821
+ return;
822
+ }
823
+ if (key.upArrow && previewTheme) {
824
+ const currentIndex = THEME_ORDER.indexOf(previewTheme);
825
+ const prevIndex = (currentIndex - 1 + THEME_ORDER.length) % THEME_ORDER.length;
826
+ setPreviewTheme(THEME_ORDER[prevIndex]);
827
+ return;
828
+ }
829
+ if (key.downArrow && previewTheme) {
830
+ const currentIndex = THEME_ORDER.indexOf(previewTheme);
831
+ const nextIndex = (currentIndex + 1) % THEME_ORDER.length;
832
+ setPreviewTheme(THEME_ORDER[nextIndex]);
833
+ return;
834
+ }
835
+ return;
836
+ }
837
+ // Help/Menu view
838
+ if (view === 'help') {
839
+ if (key.escape) {
840
+ setView('dashboard');
841
+ return;
842
+ }
843
+ if (input === 'b') {
844
+ openBrowser(`http://localhost:${webPort}`);
845
+ return;
846
+ }
847
+ if (input === 'q') {
848
+ setIsShuttingDown(true);
849
+ serviceManager.stopAll().then(() => exit());
850
+ return;
851
+ }
852
+ return;
853
+ }
854
+ // Full log view mode
855
+ if (view === 'fullLog') {
856
+ if (fullLogSearchMode) {
857
+ if (key.escape || key.return) {
858
+ setFullLogSearchMode(false);
859
+ }
860
+ return;
861
+ }
862
+ if (key.escape) {
863
+ setView('dashboard');
864
+ return;
865
+ }
866
+ if (input === 'l' || input === 't') {
867
+ setView('dashboard');
868
+ return;
869
+ }
870
+ if (input === '/') {
871
+ setFullLogSearchMode(true);
872
+ return;
873
+ }
874
+ if (input === 'f') {
875
+ const modes = ['all', 'errors', 'tools', 'verbose'];
876
+ const currentIndex = modes.indexOf(filterMode);
877
+ setFilterMode(modes[(currentIndex + 1) % modes.length]);
878
+ setFullLogScrollOffset(0);
879
+ return;
880
+ }
881
+ if (key.upArrow) {
882
+ setFullLogScrollOffset(prev => Math.max(0, prev - 1));
883
+ return;
884
+ }
885
+ if (key.downArrow) {
886
+ const maxScroll = Math.max(0, fullLogFilteredLogs.length - fullLogVisibleLines);
887
+ setFullLogScrollOffset(prev => Math.min(maxScroll, prev + 1));
888
+ return;
889
+ }
890
+ if (key.pageUp) {
891
+ setFullLogScrollOffset(prev => Math.max(0, prev - fullLogVisibleLines));
892
+ return;
893
+ }
894
+ if (key.pageDown) {
895
+ const maxScroll = Math.max(0, fullLogFilteredLogs.length - fullLogVisibleLines);
896
+ setFullLogScrollOffset(prev => Math.min(maxScroll, prev + fullLogVisibleLines));
897
+ return;
898
+ }
899
+ if (input === 'q') {
900
+ setIsShuttingDown(true);
901
+ serviceManager.stopAll().then(() => exit());
902
+ return;
903
+ }
904
+ if (input === 'b') {
905
+ openBrowser(`http://localhost:${webPort}`);
906
+ return;
907
+ }
908
+ return;
909
+ }
910
+ // Dashboard mode
911
+ if (key.escape) {
912
+ if (searchMode) {
913
+ setSearchMode(false);
914
+ setSearchQuery('');
915
+ }
916
+ else if (view !== 'dashboard') {
917
+ setView('dashboard');
918
+ }
919
+ return;
920
+ }
921
+ if (searchMode)
922
+ return;
923
+ if (input === '/' && view === 'dashboard') {
924
+ setSearchMode(true);
925
+ setSearchQuery('');
926
+ return;
927
+ }
928
+ // Ctrl+T to open theme selector
929
+ if (key.ctrl && input === 't') {
930
+ openThemeSelector();
931
+ return;
932
+ }
933
+ if (input === 'q' || (key.ctrl && input === 'c')) {
934
+ setIsShuttingDown(true);
935
+ serviceManager.stopAll().then(() => exit());
936
+ }
937
+ else if (input === 'b') {
938
+ openBrowser(`http://localhost:${webPort}`);
939
+ }
940
+ else if (input === 'v') {
941
+ setIsVerbose(!isVerbose);
942
+ }
943
+ else if (input === 'r' && view === 'dashboard') {
944
+ serviceManager.restartAll();
945
+ }
946
+ else if (input === 'c' && view === 'dashboard') {
947
+ setLogs([]);
948
+ setScrollOffset(0);
949
+ setAutoScroll(true);
950
+ }
951
+ else if (input === 'l' || input === 't') {
952
+ setView('fullLog');
953
+ setFullLogScrollOffset(Math.max(0, fullLogFilteredLogs.length - fullLogVisibleLines));
954
+ }
955
+ else if (input === 'f') {
956
+ setServiceFilter(current => {
957
+ if (!current)
958
+ return 'web';
959
+ if (current === 'web')
960
+ return 'runner';
961
+ return null;
962
+ });
963
+ }
964
+ else if (key.upArrow) {
965
+ setAutoScroll(false);
966
+ setScrollOffset(prev => Math.max(0, prev - 1));
967
+ }
968
+ else if (key.downArrow) {
969
+ const maxScroll = Math.max(0, filteredLogs.length - visibleLines);
970
+ setScrollOffset(prev => Math.min(maxScroll, prev + 1));
971
+ if (scrollOffset >= maxScroll - 1) {
972
+ setAutoScroll(true);
973
+ }
974
+ }
975
+ else if (key.pageUp) {
976
+ setAutoScroll(false);
977
+ setScrollOffset(prev => Math.max(0, prev - visibleLines));
978
+ }
979
+ else if (key.pageDown) {
980
+ const maxScroll = Math.max(0, filteredLogs.length - visibleLines);
981
+ setScrollOffset(prev => Math.min(maxScroll, prev + visibleLines));
982
+ }
983
+ else if (input === '?') {
984
+ setView('help');
985
+ }
986
+ });
987
+ const highlightSearch = (text, query) => {
988
+ if (!query)
989
+ return text;
990
+ const lowerText = text.toLowerCase();
991
+ const lowerQuery = query.toLowerCase();
992
+ const index = lowerText.indexOf(lowerQuery);
993
+ if (index === -1)
994
+ return text;
995
+ return (jsxs(Fragment, { children: [text.slice(0, index), jsx(Text, { backgroundColor: baseColors.warning, color: "black", children: text.slice(index, index + query.length) }), text.slice(index + query.length)] }));
996
+ };
997
+ // Full log view
998
+ if (view === 'fullLog') {
999
+ return (jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [jsxs(Box, { borderStyle: "single", borderColor: themeColors.muted, paddingX: 1, justifyContent: "space-between", children: [jsx(Text, { color: themeColors.primary, bold: true, children: "LOGS" }), jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: "Search: " }), fullLogSearchMode ? (jsx(Box, { borderStyle: "round", borderColor: themeColors.primary, paddingX: 1, children: jsx(TextInput, { value: fullLogSearchQuery, onChange: setFullLogSearchQuery, placeholder: "type to search..." }) })) : (jsxs(Text, { color: fullLogSearchQuery ? themeColors.text : themeColors.textMuted, children: ["[", fullLogSearchQuery || 'none', "]"] })), jsx(Text, { color: themeColors.textMuted, children: " [/]" })] })] }), jsx(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "single", borderColor: themeColors.muted, borderTop: false, borderBottom: false, paddingX: 1, children: fullLogDisplayedLogs.map((log) => (jsx(FullLogEntryRow, { log: log, maxWidth: terminalWidth - 4, searchQuery: fullLogSearchQuery, highlightSearch: highlightSearch, themeColors: themeColors }, log.id))) }), jsxs(Box, { borderStyle: "single", borderColor: themeColors.muted, paddingX: 1, justifyContent: "space-between", children: [jsxs(Box, { children: [jsx(Shortcut, { letter: "l", label: "dashboard", color: themeColors.primary }), jsx(Shortcut, { letter: "/", label: "search", color: themeColors.primary }), jsx(Shortcut, { letter: "f", label: `filter: ${filterMode}`, color: themeColors.primary }), jsx(Shortcut, { letter: "b", label: "browser", color: themeColors.primary }), jsx(Shortcut, { letter: "q", label: "quit", color: themeColors.primary })] }), jsxs(Text, { color: themeColors.textMuted, children: [fullLogScrollOffset + 1, "-", Math.min(fullLogScrollOffset + fullLogVisibleLines, fullLogFilteredLogs.length), "/", fullLogFilteredLogs.length] })] })] }));
1000
+ }
1001
+ // Theme Selector overlay
1002
+ if (view === 'themeSelector') {
1003
+ return (jsxs(Box, { flexDirection: "column", height: terminalHeight, width: terminalWidth, children: [jsx(Banner, {}), jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: themeColors.primary, paddingX: 3, paddingY: 1, width: 40, children: [jsx(Box, { justifyContent: "center", marginBottom: 1, children: jsx(Text, { color: themeColors.primary, bold: true, children: "Select Theme" }) }), THEME_ORDER.map((themeName) => {
1004
+ const t = THEMES[themeName];
1005
+ const isSelected = themeName === previewTheme;
1006
+ return (jsxs(Box, { paddingY: 0, children: [jsx(Text, { color: isSelected ? t.colors.primary : themeColors.textMuted, children: isSelected ? '▸ ' : ' ' }), jsx(Text, { color: isSelected ? t.colors.primary : themeColors.textDim, bold: isSelected, children: t.label }), jsxs(Text, { color: themeColors.textMuted, children: [" - ", t.description] })] }, themeName));
1007
+ }), jsx(Box, { marginTop: 1, justifyContent: "center", children: jsxs(Text, { color: themeColors.textMuted, children: [jsx(Text, { color: themeColors.primary, children: "\u2191\u2193" }), " navigate", jsx(Text, { color: themeColors.primary, children: " Enter" }), " select", jsx(Text, { color: themeColors.primary, children: " Esc" }), " cancel"] }) })] }) })] }));
1008
+ }
1009
+ // Help/Menu view
1010
+ if (view === 'help') {
1011
+ return (jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [jsx(Banner, {}), jsxs(Box, { flexDirection: "column", padding: 2, children: [jsx(Text, { color: themeColors.primary, bold: true, children: "Help & Keyboard Shortcuts" }), jsx(Text, { children: " " }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "Ctrl+T" }), " Change theme"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "b" }), " Open in browser"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "l" }), " Full log view"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "/" }), " Search logs"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "f" }), " Filter by service"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "v" }), " Toggle verbose mode"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "r" }), " Restart services"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "c" }), " Clear logs"] }), jsxs(Text, { color: themeColors.textDim, children: [" ", jsx(Text, { color: themeColors.primary, children: "q" }), " Quit"] }), jsx(Text, { children: " " }), jsxs(Text, { color: themeColors.textMuted, children: ["Current theme: ", jsx(Text, { color: themeColors.primary, children: theme.label })] }), jsx(Text, { children: " " }), jsxs(Text, { color: themeColors.textMuted, children: ["Press ", jsx(Text, { color: themeColors.primary, children: "Esc" }), " to return to dashboard"] })] })] }));
1012
+ }
1013
+ // Check for available update (set by auto-update check in index.ts)
1014
+ const updateAvailable = process.env.OPENBUILDER_UPDATE_AVAILABLE;
1015
+ // Main dashboard view
1016
+ return (jsxs(Box, { flexDirection: "column", height: terminalHeight, width: terminalWidth, children: [jsx(Banner, {}), updateAvailable && (jsxs(Box, { justifyContent: "center", paddingY: 0, children: [jsx(Text, { color: baseColors.cyan, children: "\u2B06 Update available: " }), jsx(Text, { color: baseColors.success, children: updateAvailable }), jsx(Text, { color: themeColors.textMuted, children: " \u2014 Run " }), jsx(Text, { color: baseColors.cyan, children: "openbuilder upgrade" }), jsx(Text, { color: themeColors.textMuted, children: " to update" })] })), jsxs(Box, { borderStyle: "single", borderColor: themeColors.muted, paddingX: 1, justifyContent: "space-between", children: [jsxs(Text, { color: themeColors.textMuted, children: ["Web: ", jsxs(Text, { color: themeColors.primary, children: ["localhost:", webPort] }), ' • ', "Mode: ", jsx(Text, { color: themeColors.primary, children: "Local" }), ' • ', "Theme: ", jsx(Text, { color: themeColors.primary, children: theme.label })] }), jsxs(Box, { children: [jsx(Text, { color: allServicesRunning ? baseColors.success : baseColors.warning, children: allServicesRunning ? symbols.filledDot : symbols.hollowDot }), jsxs(Text, { color: themeColors.textDim, children: [' ', allServicesRunning ? 'All Services Running' : 'Starting...'] })] })] }), jsxs(Box, { flexGrow: 1, height: contentHeight, children: [showBuildPanel && (jsx(BuildPanel, { build: buildState.currentBuild, width: buildPanelWidth, height: contentHeight })), jsxs(Box, { flexDirection: "column", width: logPanelWidth, height: contentHeight, borderStyle: "single", borderColor: themeColors.muted, paddingX: 1, children: [jsxs(Box, { justifyContent: "space-between", marginBottom: 0, children: [jsx(Text, { color: themeColors.primary, bold: true, children: "LOGS" }), jsxs(Box, { children: [serviceFilter && (jsxs(Text, { color: themeColors.textMuted, children: ["filter: ", jsx(Text, { color: baseColors.warning, children: serviceFilter }), " "] })), jsxs(Text, { color: themeColors.textMuted, children: ["[verbose: ", isVerbose ? 'on' : 'off', "]"] })] })] }), jsx(Box, { flexDirection: "column", flexGrow: 1, children: displayedLogs.length === 0 ? (jsx(Box, { justifyContent: "center", alignItems: "center", flexGrow: 1, children: jsx(Text, { color: themeColors.textMuted, children: "Waiting for logs..." }) })) : (displayedLogs.map((log) => (jsx(LogEntryRow, { log: log, maxWidth: logPanelWidth - 4, themeColors: themeColors }, log.id)))) }), filteredLogs.length > visibleLines && (jsx(Box, { justifyContent: "flex-end", children: jsxs(Text, { color: themeColors.textMuted, children: [scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, filteredLogs.length), "/", filteredLogs.length, autoScroll ? ' (auto)' : ''] }) }))] })] }), jsxs(Box, { borderStyle: "single", borderColor: themeColors.muted, paddingX: 1, justifyContent: "space-between", children: [jsxs(Box, { children: [jsx(Text, { color: allServicesRunning ? baseColors.success : baseColors.warning, children: allServicesRunning ? symbols.filledDot : symbols.hollowDot }), jsxs(Text, { color: themeColors.textDim, children: [' ', isShuttingDown ? 'Shutting down...' : allServicesRunning ? 'Ready' : 'Starting'] })] }), searchMode ? (jsxs(Box, { children: [jsx(Text, { color: themeColors.primary, children: "/" }), jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, onSubmit: () => setSearchMode(false), placeholder: "Search... (Enter to apply, Esc to cancel)" })] })) : (jsxs(Box, { children: [jsx(Shortcut, { letter: "b", label: "browser", color: themeColors.primary }), jsx(Shortcut, { letter: "l", label: "logs", color: themeColors.primary }), jsx(Shortcut, { letter: "/", label: "search", color: themeColors.primary }), jsx(Shortcut, { letter: "?", label: "menu", color: themeColors.primary }), jsx(Shortcut, { letter: "q", label: "quit", color: themeColors.primary })] }))] })] }));
1017
+ }
1018
+ function LogEntryRow({ log, maxWidth, themeColors }) {
1019
+ const levelColors = {
1020
+ info: themeColors.primary,
1021
+ success: baseColors.success,
1022
+ warn: baseColors.warning,
1023
+ error: baseColors.error,
1024
+ };
1025
+ const levelIcons = {
1026
+ info: symbols.filledDot,
1027
+ success: symbols.check,
1028
+ warn: '⚠',
1029
+ error: symbols.cross,
1030
+ };
1031
+ const serviceColor = log.service === 'web' ? themeColors.primary : themeColors.secondary;
1032
+ const icon = log.emoji || levelIcons[log.level];
1033
+ const color = levelColors[log.level];
1034
+ // Use displayMessage if available (user-friendly transformed message)
1035
+ let displayContent = log.displayMessage || log.content || log.message;
1036
+ const availableWidth = maxWidth - 16;
1037
+ const truncatedMessage = displayContent.length > availableWidth
1038
+ ? displayContent.substring(0, availableWidth - 3) + '...'
1039
+ : displayContent;
1040
+ // Tool calls get special formatting
1041
+ if (log.toolName) {
1042
+ // If we have a displayMessage, use it (it's already user-friendly)
1043
+ if (log.displayMessage) {
1044
+ return (jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: formatTime(log.timestamp) }), jsxs(Text, { color: serviceColor, children: [" [", log.service.substring(0, 3), "]"] }), jsx(Text, { color: themeColors.primary, children: " \uD83D\uDD27 " }), jsx(Text, { color: themeColors.text, children: log.displayMessage })] }));
1045
+ }
1046
+ // Fallback to showing tool name
1047
+ return (jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: formatTime(log.timestamp) }), jsxs(Text, { color: serviceColor, children: [" [", log.service.substring(0, 3), "]"] }), jsx(Text, { color: themeColors.primary, children: " \uD83D\uDD27 " }), jsx(Text, { color: themeColors.text, children: log.toolName })] }));
1048
+ }
1049
+ return (jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: formatTime(log.timestamp) }), jsxs(Text, { color: serviceColor, children: [" [", log.service.substring(0, 3), "]"] }), jsxs(Text, { color: color, children: [" ", icon, " "] }), jsx(Text, { color: log.level === 'error' || log.level === 'warn' ? color : themeColors.text, children: truncatedMessage })] }));
1050
+ }
1051
+ function FullLogEntryRow({ log, maxWidth, searchQuery, highlightSearch, themeColors }) {
1052
+ const levelColors = {
1053
+ info: themeColors.primary,
1054
+ success: baseColors.success,
1055
+ warn: baseColors.warning,
1056
+ error: baseColors.error,
1057
+ };
1058
+ const levelIcons = {
1059
+ info: symbols.filledDot,
1060
+ success: symbols.check,
1061
+ warn: '⚠',
1062
+ error: symbols.cross,
1063
+ };
1064
+ const serviceColor = log.service === 'web' ? themeColors.primary : themeColors.secondary;
1065
+ const icon = log.emoji || levelIcons[log.level];
1066
+ const color = levelColors[log.level];
1067
+ if (log.toolName) {
1068
+ return (jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: formatTime(log.timestamp) }), jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), jsx(Text, { color: themeColors.primary, children: " \uD83D\uDD27 " }), jsx(Text, { color: themeColors.text, children: highlightSearch(log.toolName, searchQuery) }), log.content && (jsxs(Text, { color: themeColors.textDim, children: [" ", highlightSearch(log.content.replace(`tool-input-available: ${log.toolName}`, '').trim(), searchQuery)] }))] }));
1069
+ }
1070
+ return (jsxs(Box, { children: [jsx(Text, { color: themeColors.textMuted, children: formatTime(log.timestamp) }), jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), jsxs(Text, { color: color, children: [" ", icon, " "] }), log.tag && jsxs(Text, { color: themeColors.textMuted, children: ["[", log.tag, "] "] }), jsx(Text, { color: log.level === 'error' || log.level === 'warn' ? color : themeColors.text, children: highlightSearch(log.content || log.message, searchQuery) })] }));
1071
+ }
1072
+ function Shortcut({ letter, label, color }) {
1073
+ return (jsxs(Box, { marginRight: 2, children: [jsx(Text, { color: baseColors.dimGray, children: "[" }), jsx(Text, { color: color, children: letter }), jsx(Text, { color: baseColors.dimGray, children: "]" }), jsx(Text, { color: baseColors.gray, children: label })] }));
1074
+ }
1075
+
1076
+ /**
1077
+ * Console Interceptor for TUI Mode
1078
+ * Intercepts stdout/stderr writes to write to log file
1079
+ * Prevents logs from bleeding above the TUI
1080
+ */
1081
+ class ConsoleInterceptor {
1082
+ constructor(serviceManager, logFileManager) {
1083
+ this.isActive = false;
1084
+ this.serviceManager = serviceManager;
1085
+ this.logFileManager = logFileManager;
1086
+ // Save original stdout/stderr write methods
1087
+ // Console methods (log, error, warn, info) all use these under the hood
1088
+ this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
1089
+ this.originalStderrWrite = process.stderr.write.bind(process.stderr);
1090
+ }
1091
+ /**
1092
+ * Start intercepting console output and writing to log file
1093
+ * We only intercept stdout/stderr writes since console.* methods call these under the hood
1094
+ */
1095
+ start() {
1096
+ if (this.isActive)
1097
+ return;
1098
+ this.isActive = true;
1099
+ // Intercept stdout writes - this catches console.log, console.info, and direct writes
1100
+ process.stdout.write = (chunk, encoding, callback) => {
1101
+ const message = typeof chunk === 'string' ? chunk : chunk.toString();
1102
+ // ONLY pass through ANSI escape codes (for Ink rendering)
1103
+ // Block all other output from reaching the terminal
1104
+ if (message.match(/^\x1b\[/)) {
1105
+ // This is Ink's control sequence - let it through
1106
+ return this.originalStdoutWrite(chunk, encoding, callback);
1107
+ }
1108
+ // Everything else: write to log file only (don't show on terminal)
1109
+ // Don't trim or filter - write the raw message to preserve formatting
1110
+ if (message && message.length > 0) {
1111
+ const serviceName = this.detectService(message);
1112
+ this.logFileManager.write(serviceName, message.trim(), 'stdout');
1113
+ }
1114
+ // Report success but don't actually write to terminal
1115
+ if (callback)
1116
+ callback();
1117
+ return true;
1118
+ };
1119
+ // Intercept stderr writes
1120
+ process.stderr.write = (chunk, encoding, callback) => {
1121
+ const message = typeof chunk === 'string' ? chunk : chunk.toString();
1122
+ // ONLY pass through ANSI escape codes (for Ink rendering)
1123
+ // Block all other output from reaching the terminal
1124
+ if (message.match(/^\x1b\[/)) {
1125
+ // This is Ink's control sequence - let it through
1126
+ return this.originalStderrWrite(chunk, encoding, callback);
1127
+ }
1128
+ // Everything else: write to log file only (don't show on terminal)
1129
+ // Don't trim or filter - write the raw message to preserve formatting
1130
+ if (message && message.length > 0) {
1131
+ const serviceName = this.detectService(message);
1132
+ this.logFileManager.write(serviceName, message.trim(), 'stderr');
1133
+ }
1134
+ // Report success but don't actually write to terminal
1135
+ if (callback)
1136
+ callback();
1137
+ return true;
1138
+ };
1139
+ }
1140
+ /**
1141
+ * Get the log file path (null if logging is disabled)
1142
+ */
1143
+ getLogFilePath() {
1144
+ return this.logFileManager.getLogFilePath();
1145
+ }
1146
+ /**
1147
+ * Stop intercepting and restore original stdout/stderr
1148
+ */
1149
+ stop() {
1150
+ if (!this.isActive)
1151
+ return;
1152
+ this.isActive = false;
1153
+ // Restore stdout/stderr - this also restores console.* methods since they use these
1154
+ process.stdout.write = this.originalStdoutWrite;
1155
+ process.stderr.write = this.originalStderrWrite;
1156
+ // Stop log file writing
1157
+ this.logFileManager.stop();
1158
+ }
1159
+ /**
1160
+ * Detect which service the log is from based on content
1161
+ */
1162
+ detectService(message) {
1163
+ const lower = message.toLowerCase();
1164
+ // Check for explicit service tags
1165
+ if (message.startsWith('[web]'))
1166
+ return 'web';
1167
+ if (message.startsWith('[broker]'))
1168
+ return 'broker';
1169
+ if (message.startsWith('[runner]') || message.startsWith('[build]') || message.startsWith('[orchestrator]') || message.startsWith('[engine]')) {
1170
+ return 'runner';
1171
+ }
1172
+ // Infer from content
1173
+ if (lower.includes('broker') || lower.includes('websocket'))
1174
+ return 'broker';
1175
+ if (lower.includes('next.js') || lower.includes('compiled') || lower.includes('ready in'))
1176
+ return 'web';
1177
+ // Default to runner (most console.log calls come from runner)
1178
+ return 'runner';
1179
+ }
1180
+ }
1181
+
1182
+ /**
1183
+ * Log File Manager
1184
+ * Writes logs to a file for TUI to read periodically
1185
+ * Creates a new log file each time the service starts
1186
+ */
1187
+ class LogFileManager {
1188
+ constructor() {
1189
+ this.logFile = null;
1190
+ this.writeStream = null;
1191
+ // Create logs directory if it doesn't exist
1192
+ const logsDir = join(process.cwd(), 'logs');
1193
+ if (!existsSync(logsDir)) {
1194
+ mkdirSync(logsDir, { recursive: true });
1195
+ }
1196
+ // Create log file with timestamp
1197
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1198
+ this.logFile = join(logsDir, `openbuilder-${timestamp}.log`);
1199
+ }
1200
+ /**
1201
+ * Check if logging is enabled (always true now)
1202
+ */
1203
+ isEnabled() {
1204
+ return true;
1205
+ }
1206
+ /**
1207
+ * Start writing to log file
1208
+ */
1209
+ start() {
1210
+ if (!this.logFile)
1211
+ return;
1212
+ this.writeStream = createWriteStream(this.logFile, { flags: 'a' });
1213
+ // Write a startup marker
1214
+ this.writeStream.write(`[${new Date().toISOString()}] [system] [stdout] === Log file started ===\n`);
1215
+ }
1216
+ /**
1217
+ * Write a log entry to file
1218
+ * Format: [timestamp] [service] [stream] message
1219
+ */
1220
+ write(service, message, stream) {
1221
+ if (!this.writeStream)
1222
+ return;
1223
+ const timestamp = new Date().toISOString();
1224
+ const logLine = `[${timestamp}] [${service}] [${stream}] ${message}\n`;
1225
+ this.writeStream.write(logLine, (err) => {
1226
+ if (err) {
1227
+ // Fallback to console if write fails (but this shouldn't happen often)
1228
+ console.error('Failed to write to log file:', err);
1229
+ }
1230
+ });
1231
+ }
1232
+ /**
1233
+ * Get the log file path (null if logging is disabled)
1234
+ */
1235
+ getLogFilePath() {
1236
+ return this.logFile;
1237
+ }
1238
+ /**
1239
+ * Stop writing and close the file stream
1240
+ */
1241
+ stop() {
1242
+ if (this.writeStream) {
1243
+ this.writeStream.end();
1244
+ this.writeStream = null;
1245
+ }
1246
+ }
1247
+ }
1248
+
1249
+ /**
1250
+ * Utility to extract meaningful error messages from build output
1251
+ * Works with TypeScript, Next.js, Turbo, and other common build tools
1252
+ */
1253
+ // Patterns that indicate error lines
1254
+ const ERROR_PATTERNS = [
1255
+ /error TS\d+:/i, // TypeScript errors
1256
+ /error:/i, // General errors
1257
+ /Error:/, // Error messages (case sensitive for JS errors)
1258
+ /ERR!/, // npm/pnpm errors
1259
+ /failed/i, // Failed messages
1260
+ /Cannot find/i, // Module not found
1261
+ /Module not found/i, // Webpack/Next.js errors
1262
+ /SyntaxError/i, // Syntax errors
1263
+ /TypeError/i, // Type errors
1264
+ /ReferenceError/i, // Reference errors
1265
+ /ENOENT/i, // File not found
1266
+ /EACCES/i, // Permission errors
1267
+ /✖|✗|×/, // Error symbols
1268
+ /Type '.+' is not assignable/i, // TypeScript type errors
1269
+ /Property '.+' does not exist/i, // TypeScript property errors
1270
+ /has no exported member/i, // Export errors
1271
+ /Unexpected token/i, // Parse errors
1272
+ ];
1273
+ // Patterns that indicate we should stop collecting (success or unrelated output)
1274
+ const STOP_PATTERNS = [
1275
+ /successfully/i,
1276
+ /completed/i,
1277
+ /✓|✔/, // Success symbols
1278
+ /Build succeeded/i,
1279
+ ];
1280
+ /**
1281
+ * Extract the most relevant error lines from build output
1282
+ * @param output Combined stdout and stderr from build process
1283
+ * @param maxLines Maximum number of lines to return (default: 15)
1284
+ * @returns Array of relevant error lines
1285
+ */
1286
+ function extractBuildErrors(output, maxLines = 15) {
1287
+ if (!output || !output.trim()) {
1288
+ return [];
1289
+ }
1290
+ const allLines = output.trim().split('\n');
1291
+ const relevantLines = [];
1292
+ let inErrorBlock = false;
1293
+ let emptyLineCount = 0;
1294
+ for (let i = 0; i < allLines.length; i++) {
1295
+ const line = allLines[i];
1296
+ const trimmedLine = line.trim();
1297
+ // Check if this line matches an error pattern
1298
+ const isErrorLine = ERROR_PATTERNS.some(pattern => pattern.test(line));
1299
+ // Check if we should stop collecting
1300
+ const shouldStop = STOP_PATTERNS.some(pattern => pattern.test(line));
1301
+ if (shouldStop && inErrorBlock) {
1302
+ // Don't stop immediately - there might be more errors after a success message
1303
+ inErrorBlock = false;
1304
+ emptyLineCount = 0;
1305
+ continue;
1306
+ }
1307
+ if (isErrorLine) {
1308
+ inErrorBlock = true;
1309
+ emptyLineCount = 0;
1310
+ // Include 1-2 lines before for context if we're starting a new error block
1311
+ if (relevantLines.length === 0 || !relevantLines[relevantLines.length - 1]) {
1312
+ // Add file path context if the previous line looks like a file reference
1313
+ if (i > 0) {
1314
+ const prevLine = allLines[i - 1].trim();
1315
+ // File paths often contain '/' or '\' and end with line numbers like :10:5
1316
+ if (prevLine && (prevLine.includes('/') || prevLine.includes('\\') || /:\d+:\d+/.test(prevLine))) {
1317
+ if (!relevantLines.includes(prevLine)) {
1318
+ relevantLines.push(prevLine);
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+ if (inErrorBlock) {
1325
+ // Track empty lines - stop after 2 consecutive empty lines
1326
+ if (trimmedLine === '') {
1327
+ emptyLineCount++;
1328
+ if (emptyLineCount >= 2) {
1329
+ inErrorBlock = false;
1330
+ emptyLineCount = 0;
1331
+ continue;
1332
+ }
1333
+ }
1334
+ else {
1335
+ emptyLineCount = 0;
1336
+ }
1337
+ // Don't add duplicate lines
1338
+ if (!relevantLines.includes(trimmedLine) || trimmedLine === '') {
1339
+ relevantLines.push(trimmedLine);
1340
+ }
1341
+ // Stop if we've collected enough
1342
+ if (relevantLines.length >= maxLines) {
1343
+ break;
1344
+ }
1345
+ }
1346
+ }
1347
+ // If we didn't find specific error patterns, fall back to last N lines
1348
+ if (relevantLines.length === 0) {
1349
+ return allLines
1350
+ .slice(-10)
1351
+ .map(line => line.trim())
1352
+ .filter(line => line.length > 0);
1353
+ }
1354
+ // Clean up: remove trailing empty lines
1355
+ while (relevantLines.length > 0 && relevantLines[relevantLines.length - 1] === '') {
1356
+ relevantLines.pop();
1357
+ }
1358
+ // Truncate very long lines but keep enough context
1359
+ return relevantLines.map(line => {
1360
+ if (line.length > 150) {
1361
+ return line.substring(0, 147) + '...';
1362
+ }
1363
+ return line;
1364
+ });
1365
+ }
1366
+
1367
+ /**
1368
+ * Start command with TUI dashboard support
1369
+ * Provides beautiful real-time monitoring of all services
1370
+ */
1371
+ /**
1372
+ * Check if we should use TUI
1373
+ */
1374
+ function shouldUseTUI(options) {
1375
+ // Explicit flag
1376
+ if (options.noTui)
1377
+ return false;
1378
+ // CI/CD environments
1379
+ if (process.env.CI === '1' || process.env.CI === 'true')
1380
+ return false;
1381
+ // Not a TTY
1382
+ if (!process.stdout.isTTY)
1383
+ return false;
1384
+ // Explicit env var to disable
1385
+ if (process.env.NO_TUI === '1')
1386
+ return false;
1387
+ return true;
1388
+ }
1389
+ async function startCommand(options) {
1390
+ const useTUI = shouldUseTUI(options);
1391
+ // If TUI is disabled, use the traditional start command
1392
+ if (!useTUI) {
1393
+ const { startCommand: traditionalStart } = await import('./start-traditional-uoLZXdxm.js');
1394
+ return traditionalStart(options);
1395
+ }
1396
+ // ========================================
1397
+ // TUI MODE
1398
+ // ========================================
1399
+ const s = p.spinner();
1400
+ // Step 1: Find monorepo
1401
+ s.start('Locating OpenBuilder repository');
1402
+ let monorepoRoot;
1403
+ const config = configManager.get();
1404
+ if (config.monorepoPath && existsSync(config.monorepoPath)) {
1405
+ monorepoRoot = config.monorepoPath;
1406
+ }
1407
+ if (!monorepoRoot) {
1408
+ const repoCheck = await isInsideMonorepo();
1409
+ if (repoCheck.inside && repoCheck.root) {
1410
+ monorepoRoot = repoCheck.root;
1411
+ configManager.set('monorepoPath', monorepoRoot);
1412
+ }
1413
+ }
1414
+ if (!monorepoRoot) {
1415
+ s.stop(pc.red('✗') + ' Repository not found');
1416
+ throw errors.monorepoNotFound([
1417
+ config.monorepoPath || 'none',
1418
+ process.cwd(),
1419
+ ]);
1420
+ }
1421
+ s.stop(pc.green('✓') + ' Repository found');
1422
+ // Step 2: Check dependencies
1423
+ const nodeModulesPath = join(monorepoRoot, 'node_modules');
1424
+ if (!existsSync(nodeModulesPath)) {
1425
+ s.start('Installing dependencies');
1426
+ const { installDependencies } = await import('./repo-cloner-CpOQjFSo.js');
1427
+ await installDependencies(monorepoRoot);
1428
+ s.stop(pc.green('✓') + ' Dependencies installed');
1429
+ }
1430
+ // Step 2.5: Check for production build (unless --dev mode)
1431
+ const nextBuildIdPath = join(monorepoRoot, 'apps', 'openbuilder', '.next', 'BUILD_ID');
1432
+ const needsProductionBuild = !options.dev && !existsSync(nextBuildIdPath);
1433
+ // Rebuild services if requested OR if production build is missing
1434
+ if (options.rebuild || needsProductionBuild) {
1435
+ const buildReason = options.rebuild
1436
+ ? 'Rebuilding services'
1437
+ : 'Building for production (first run)';
1438
+ s.start(buildReason);
1439
+ const { spawn } = await import('child_process');
1440
+ // Capture build output for error reporting
1441
+ let buildOutput = '';
1442
+ let buildError = '';
1443
+ try {
1444
+ // Use turbo to build all services with caching
1445
+ await new Promise((resolve, reject) => {
1446
+ const buildProcess = spawn('pnpm', ['build:all'], {
1447
+ cwd: monorepoRoot,
1448
+ stdio: 'pipe', // Capture output
1449
+ });
1450
+ buildProcess.stdout?.on('data', (data) => {
1451
+ buildOutput += data.toString();
1452
+ });
1453
+ buildProcess.stderr?.on('data', (data) => {
1454
+ buildError += data.toString();
1455
+ });
1456
+ buildProcess.on('close', (code) => {
1457
+ if (code === 0) {
1458
+ resolve();
1459
+ }
1460
+ else {
1461
+ reject(new Error(`Build failed with code ${code}`));
1462
+ }
1463
+ });
1464
+ buildProcess.on('error', reject);
1465
+ });
1466
+ s.stop(pc.green('✓') + ' Build complete (using Turborepo cache)');
1467
+ }
1468
+ catch (error) {
1469
+ s.stop(pc.red('✗') + ' Build failed');
1470
+ // Extract meaningful error lines from build output
1471
+ const allOutput = (buildOutput + '\n' + buildError).trim();
1472
+ const errorLines = extractBuildErrors(allOutput);
1473
+ const suggestions = [
1474
+ 'Check that all dependencies are installed',
1475
+ 'Try running: pnpm build:all',
1476
+ 'Run with --dev flag to skip build and use dev mode',
1477
+ ];
1478
+ // Add error context if available
1479
+ if (errorLines.length > 0) {
1480
+ console.log(pc.red('\nBuild errors:'));
1481
+ console.log(pc.gray('─'.repeat(60)));
1482
+ errorLines.forEach(line => console.log(pc.red(` ${line}`)));
1483
+ console.log(pc.gray('─'.repeat(60)));
1484
+ console.log('');
1485
+ }
1486
+ throw new CLIError({
1487
+ code: 'BUILD_FAILED',
1488
+ message: 'Failed to build services',
1489
+ suggestions,
1490
+ });
1491
+ }
1492
+ }
1493
+ // Step 3: Check database configuration
1494
+ if (!config.databaseUrl) {
1495
+ throw new CLIError({
1496
+ code: 'MISSING_REQUIRED_CONFIG',
1497
+ message: 'Database URL not configured',
1498
+ suggestions: [
1499
+ 'Run initialization: openbuilder init',
1500
+ 'Or set manually: openbuilder config set databaseUrl <url>',
1501
+ ],
1502
+ docs: 'https://github.com/codyde/openbuilder#database-setup',
1503
+ });
1504
+ }
1505
+ // Step 4: Clean up zombie processes
1506
+ const webPort = Number(options.port || '3000');
1507
+ s.start('Checking for port conflicts');
1508
+ await killProcessOnPort(webPort);
1509
+ s.stop(pc.green('✓') + ' Ports available');
1510
+ // Step 5: Create ServiceManager, LogFileManager, and Console Interceptor FIRST
1511
+ const serviceManager = new ServiceManager();
1512
+ const logFileManager = new LogFileManager();
1513
+ logFileManager.start(); // Start log file writing immediately
1514
+ const consoleInterceptor = new ConsoleInterceptor(serviceManager, logFileManager);
1515
+ // Start intercepting IMMEDIATELY before anything else can print
1516
+ consoleInterceptor.start();
1517
+ const sharedSecret = configManager.getSecret() || 'dev-secret';
1518
+ // Hook up service manager output to log file (for child process logs)
1519
+ serviceManager.on('service:output', (name, output, stream) => {
1520
+ logFileManager.write(name, output.trim(), stream);
1521
+ });
1522
+ // Keep silent mode for TUI, logs go to file
1523
+ process.env.SILENT_MODE = '1';
1524
+ // Clear screen for clean TUI start
1525
+ console.clear();
1526
+ // Register web app (now handles runner WebSocket connections directly)
1527
+ // Default to production mode unless --dev flag is present
1528
+ const webCommand = options.dev ? 'dev' : 'start';
1529
+ // Local mode is enabled by default, can be disabled with --no-local
1530
+ const isLocalMode = options.local !== false;
1531
+ // Write .env.local file to ensure env vars are available to Next.js
1532
+ // This is necessary because env vars passed to spawn() may not be visible
1533
+ // to Next.js server components in production mode
1534
+ const envLocalPath = join(monorepoRoot, 'apps', 'openbuilder', '.env.local');
1535
+ const envContent = [
1536
+ '# Auto-generated by openbuilder CLI - DO NOT EDIT',
1537
+ `# Generated at: ${new Date().toISOString()}`,
1538
+ '',
1539
+ `OPENBUILDER_LOCAL_MODE=${isLocalMode ? 'true' : 'false'}`,
1540
+ `RUNNER_SHARED_SECRET=${sharedSecret}`,
1541
+ `WORKSPACE_ROOT=${config.workspace}`,
1542
+ `RUNNER_ID=${config.runner?.id || 'local'}`,
1543
+ `RUNNER_DEFAULT_ID=${config.runner?.id || 'local'}`,
1544
+ `DATABASE_URL=${config.databaseUrl || ''}`,
1545
+ '',
1546
+ ].join('\n');
1547
+ writeFileSync(envLocalPath, envContent);
1548
+ // Build environment variables for the web app
1549
+ const webEnv = {
1550
+ PORT: String(webPort),
1551
+ RUNNER_SHARED_SECRET: sharedSecret,
1552
+ WORKSPACE_ROOT: config.workspace,
1553
+ RUNNER_ID: config.runner?.id || 'local',
1554
+ RUNNER_DEFAULT_ID: config.runner?.id || 'local',
1555
+ DATABASE_URL: config.databaseUrl,
1556
+ // Enable local mode - bypasses authentication requirements (default: true)
1557
+ OPENBUILDER_LOCAL_MODE: isLocalMode ? 'true' : 'false',
1558
+ };
1559
+ serviceManager.register({
1560
+ name: 'web',
1561
+ displayName: 'Web App',
1562
+ port: webPort,
1563
+ command: 'pnpm',
1564
+ args: ['--filter', 'openbuilder', webCommand],
1565
+ cwd: monorepoRoot,
1566
+ env: webEnv,
1567
+ });
1568
+ // Register runner (special handling - not spawned, imported directly)
1569
+ serviceManager.register({
1570
+ name: 'runner',
1571
+ displayName: 'Runner',
1572
+ command: 'internal', // Not actually spawned
1573
+ args: [],
1574
+ cwd: monorepoRoot,
1575
+ env: {},
1576
+ });
1577
+ // Step 6: Clear screen and move cursor to home before TUI renders
1578
+ // Use ANSI codes that will pass through our interceptor
1579
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear screen + move cursor to top-left
1580
+ // Ensure stdin is in raw mode for keyboard input
1581
+ if (process.stdin.setRawMode) {
1582
+ process.stdin.setRawMode(true);
1583
+ }
1584
+ process.stdin.resume();
1585
+ // Track runner cleanup function and shutting down state
1586
+ let isShuttingDown = false;
1587
+ let runnerCleanupFn;
1588
+ // Add backup SIGINT handler for Ctrl+C (in case Ink's doesn't fire)
1589
+ const handleSigInt = async () => {
1590
+ if (isShuttingDown) {
1591
+ // Force exit if already shutting down
1592
+ process.exit(1);
1593
+ }
1594
+ isShuttingDown = true;
1595
+ console.log('\n⚠ Received Ctrl+C, stopping services...');
1596
+ // Stop runner first if cleanup function exists
1597
+ if (runnerCleanupFn) {
1598
+ await runnerCleanupFn().catch(() => { });
1599
+ }
1600
+ // Then stop other services
1601
+ await serviceManager.stopAll().catch(() => { });
1602
+ consoleInterceptor.stop();
1603
+ process.exit(0);
1604
+ };
1605
+ process.on('SIGINT', handleSigInt);
1606
+ // One final clear right before rendering
1607
+ process.stdout.write('\x1b[2J\x1b[H');
1608
+ // Enable alternate screen buffer to prevent scrolling above TUI
1609
+ process.stdout.write('\x1b[?1049h'); // Enter alternate screen
1610
+ process.stdout.write('\x1b[2J\x1b[H'); // Clear and home
1611
+ // Initialize the RunnerLogger BEFORE rendering TUI so the TUI can subscribe to build events
1612
+ // This must happen before startRunner() which would create its own logger
1613
+ initRunnerLogger({
1614
+ verbose: options.verbose || false,
1615
+ tuiMode: true,
1616
+ });
1617
+ // Enable TUI mode in file-logger to suppress terminal output
1618
+ setFileLoggerTuiMode(true);
1619
+ // Render TUI immediately with log file path
1620
+ const { waitUntilExit, clear } = render(React.createElement(Dashboard, {
1621
+ serviceManager,
1622
+ apiUrl: `http://localhost:${webPort}`,
1623
+ webPort,
1624
+ logFilePath: consoleInterceptor.getLogFilePath()
1625
+ }), {
1626
+ stdin: process.stdin,
1627
+ stdout: process.stdout,
1628
+ stderr: process.stderr,
1629
+ exitOnCtrlC: true,
1630
+ patchConsole: false // Don't let Ink patch console since we already intercept
1631
+ });
1632
+ // Step 7: Start services
1633
+ try {
1634
+ // Start web app (runner will be started separately)
1635
+ await serviceManager.start('web');
1636
+ await new Promise(resolve => setTimeout(resolve, 3000)); // Wait for WebSocket server to initialize
1637
+ // Mark runner as starting
1638
+ const runnerService = serviceManager['services'].get('runner');
1639
+ if (runnerService) {
1640
+ runnerService.state.status = 'starting';
1641
+ runnerService.startTime = Date.now();
1642
+ serviceManager.emit('service:status-change', 'runner', 'starting');
1643
+ }
1644
+ // Start runner directly (this blocks until shutdown)
1645
+ const { startRunner } = await import('../index.js');
1646
+ // Mark as running
1647
+ if (runnerService) {
1648
+ runnerService.state.status = 'running';
1649
+ serviceManager.emit('service:status-change', 'runner', 'running');
1650
+ }
1651
+ // Start runner and get cleanup function - connects directly to Next.js WebSocket
1652
+ runnerCleanupFn = await startRunner({
1653
+ wsUrl: `ws://localhost:${webPort}/ws/runner`,
1654
+ sharedSecret: sharedSecret,
1655
+ runnerId: config.runner?.id || 'local',
1656
+ workspace: config.workspace,
1657
+ silent: false, // Changed to false - show all logs
1658
+ verbose: options.verbose,
1659
+ tuiMode: true, // TUI mode enabled
1660
+ });
1661
+ // Wait for TUI to exit
1662
+ await waitUntilExit();
1663
+ // Stop console interception and restore normal console
1664
+ consoleInterceptor.stop();
1665
+ // Clear TUI immediately
1666
+ clear();
1667
+ // Exit alternate screen buffer
1668
+ process.stdout.write('\x1b[?1049l');
1669
+ // Show shutdown message
1670
+ console.log();
1671
+ console.log(pc.yellow('⚠'), 'Stopping all services...');
1672
+ // Stop all services with timeout
1673
+ const shutdownPromise = Promise.race([
1674
+ (async () => {
1675
+ // Close any active tunnels first (give it 1s)
1676
+ await serviceManager.closeTunnel('web').catch(() => { });
1677
+ // Stop spawned services (web, broker)
1678
+ await serviceManager.stopAll();
1679
+ console.log(pc.green('✓'), 'Services stopped');
1680
+ // Stop runner explicitly using cleanup function
1681
+ if (runnerCleanupFn) {
1682
+ console.log(pc.yellow('⚠'), 'Stopping runner...');
1683
+ await runnerCleanupFn();
1684
+ console.log(pc.green('✓'), 'Runner stopped');
1685
+ }
1686
+ console.log(pc.green('✓'), 'All services stopped');
1687
+ })(),
1688
+ new Promise((resolve) => setTimeout(() => {
1689
+ console.log(pc.yellow('⚠'), 'Shutdown timeout - forcing exit');
1690
+ resolve(undefined);
1691
+ }, 5000)) // Increased from 3s to 5s to allow tunnel cleanup
1692
+ ]);
1693
+ await shutdownPromise;
1694
+ // Force exit to ensure we return to prompt
1695
+ process.exit(0);
1696
+ }
1697
+ catch (error) {
1698
+ // Stop console interception on error
1699
+ consoleInterceptor.stop();
1700
+ clear();
1701
+ // Exit alternate screen buffer
1702
+ process.stdout.write('\x1b[?1049l');
1703
+ throw error;
1704
+ }
1705
+ }
1706
+
1707
+ export { startCommand };
1708
+ //# sourceMappingURL=start-BygPCbvw.js.map