bgrun 3.12.11 → 3.12.13

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 (50) hide show
  1. package/README.md +2 -2
  2. package/dashboard/app/api/config/[name]/route.ts +1 -1
  3. package/dashboard/app/api/debug/route.ts +1 -1
  4. package/dashboard/app/api/dependencies/route.ts +40 -40
  5. package/dashboard/app/api/deploy/[name]/route.ts +1 -1
  6. package/dashboard/app/api/deploy-all/route.ts +25 -25
  7. package/dashboard/app/api/deps/route.ts +3 -3
  8. package/dashboard/app/api/guard/route.ts +1 -1
  9. package/dashboard/app/api/guard-all/route.ts +1 -1
  10. package/dashboard/app/api/guard-events/route.ts +4 -4
  11. package/dashboard/app/api/history/route.ts +105 -105
  12. package/dashboard/app/api/logs/[name]/route.ts +100 -100
  13. package/dashboard/app/api/logs/rotate/route.ts +2 -2
  14. package/dashboard/app/api/next-port/route.ts +32 -32
  15. package/dashboard/app/api/processes/[name]/route.ts +2 -2
  16. package/dashboard/app/api/processes/route.ts +4 -4
  17. package/dashboard/app/api/restart/[name]/route.ts +2 -2
  18. package/dashboard/app/api/start/route.ts +2 -2
  19. package/dashboard/app/api/stop/[name]/route.ts +2 -2
  20. package/dashboard/app/api/templates/route.ts +46 -46
  21. package/dashboard/app/api/version/route.ts +1 -1
  22. package/dashboard/lib/runtime.ts +49 -0
  23. package/dist/api.js +94 -67
  24. package/dist/deploy.js +1373 -0
  25. package/dist/deps.js +1004 -0
  26. package/dist/index.js +224 -224
  27. package/dist/log-rotation.js +95 -0
  28. package/dist/server.js +1488 -0
  29. package/package.json +2 -17
  30. package/src/api.ts +0 -63
  31. package/src/build.ts +0 -24
  32. package/src/commands/cleanup.ts +0 -141
  33. package/src/commands/details.ts +0 -60
  34. package/src/commands/list.ts +0 -133
  35. package/src/commands/logs.ts +0 -49
  36. package/src/commands/run.ts +0 -217
  37. package/src/commands/watch.ts +0 -223
  38. package/src/config.ts +0 -37
  39. package/src/db.ts +0 -422
  40. package/src/deploy.ts +0 -163
  41. package/src/deps.ts +0 -126
  42. package/src/guard.ts +0 -208
  43. package/src/index.ts +0 -623
  44. package/src/log-rotation.ts +0 -93
  45. package/src/logger.ts +0 -40
  46. package/src/platform.ts +0 -665
  47. package/src/server.ts +0 -217
  48. package/src/table.ts +0 -232
  49. package/src/types.ts +0 -14
  50. package/src/utils.ts +0 -96
package/src/server.ts DELETED
@@ -1,217 +0,0 @@
1
- /**
2
- * BGR Dashboard Server + Built-in Process Guard
3
- *
4
- * Uses Melina.js to serve the dashboard app with file-based routing.
5
- * All API endpoints and page rendering are handled by the dashboard/app/ directory.
6
- *
7
- * v3.0: Built-in guard loop — the dashboard now monitors ALL registered
8
- * processes and auto-restarts any that die. This eliminates the need for
9
- * external guard scripts. The dashboard itself survives because bgrun
10
- * registers it as a managed process on launch.
11
- *
12
- * Port selection is handled entirely by Melina:
13
- * - If BUN_PORT env var is set → uses that (explicit, will fail if busy)
14
- * - Otherwise → defaults to 3000, falls back to next available if busy
15
- */
16
- import path from 'path';
17
- import { getAllProcesses, getProcess, addHistoryEntry } from './db';
18
- import { isProcessRunning } from './platform';
19
- import { handleRun } from './commands/run';
20
- import { parseEnvString } from './utils';
21
-
22
- const GUARD_INTERVAL_MS = 30_000; // Check every 30 seconds
23
- const GUARD_SKIP_NAMES = new Set(['bgr-dashboard', 'bgr-guard']); // Don't try to restart ourselves or external guard
24
-
25
- // In-memory guard restart counter and timestamps (persists across module re-evaluations)
26
- const _g = globalThis as any;
27
- if (!_g.__bgrGuardRestartCounts) _g.__bgrGuardRestartCounts = new Map<string, number>();
28
- if (!_g.__bgrGuardNextRestartTime) _g.__bgrGuardNextRestartTime = new Map<string, number>();
29
- if (!_g.__bgrGuardEvents) _g.__bgrGuardEvents = [] as { time: number; name: string; action: string; success: boolean }[];
30
- export const guardRestartCounts: Map<string, number> = _g.__bgrGuardRestartCounts;
31
- const guardNextRestartTime: Map<string, number> = _g.__bgrGuardNextRestartTime;
32
- export const guardEvents: { time: number; name: string; action: string; success: boolean }[] = _g.__bgrGuardEvents;
33
-
34
- /** Try to free a port from zombie processes (dead PIDs holding sockets) */
35
- async function cleanupPort(port: number): Promise<number> {
36
- if (process.platform !== 'win32') return port;
37
- try {
38
- const proc = Bun.spawn(['powershell', '-NoProfile', '-Command',
39
- `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
40
- ], { stdout: 'pipe', stderr: 'pipe' });
41
- const text = await new Response(proc.stdout).text();
42
- const pid = parseInt(text.trim(), 10);
43
- if (!pid || pid === process.pid) return port;
44
-
45
- // Check if the owning process is actually dead
46
- const checkProc = Bun.spawn(['powershell', '-NoProfile', '-Command',
47
- `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
48
- ], { stdout: 'pipe', stderr: 'pipe' });
49
- const checkText = await new Response(checkProc.stdout).text();
50
- if (checkText.trim()) {
51
- // Process is alive — kill it to reclaim the port
52
- console.log(`[server] Killing PID ${pid} holding port ${port}`);
53
- Bun.spawn(['taskkill', '/F', '/PID', String(pid)], { stdout: 'pipe', stderr: 'pipe' });
54
- await Bun.sleep(1000);
55
- return port;
56
- } else {
57
- // Process is dead but socket is zombie — use fallback port
58
- const fallback = port + 1;
59
- console.log(`[server] ⚠ Port ${port} held by zombie PID ${pid} — falling back to port ${fallback}`);
60
- return fallback;
61
- }
62
- } catch { return port; /* best-effort cleanup */ }
63
- }
64
-
65
- let _originalPort = 3000;
66
- let _currentPort = 3000;
67
-
68
- export async function startServer() {
69
- // Dynamic import to avoid melina's side-effect console.log at bundle load time
70
- const { start } = await import('melina');
71
- const appDir = path.join(import.meta.dir, '../dashboard/app');
72
-
73
- // Only pass port when BUN_PORT is explicitly set.
74
- // When omitted, Melina defaults to 3000 with auto-fallback to next available port.
75
- const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
76
- _originalPort = requestedPort;
77
-
78
- // Clean up zombie port bindings before starting — may return a different fallback port
79
- const resolvedPort = await cleanupPort(requestedPort);
80
- _currentPort = resolvedPort;
81
-
82
- // Pass port explicitly if user requested one OR if we had to fallback
83
- const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
84
- await start({
85
- appDir,
86
- defaultTitle: 'bgrun Dashboard - Process Manager',
87
- globalCss: path.join(appDir, 'globals.css'),
88
- ...(needsExplicitPort && { port: resolvedPort }),
89
- });
90
-
91
- // Start the built-in process guard
92
- startGuard();
93
-
94
- // Start log rotation (prevents unbounded log file growth)
95
- const { startLogRotation } = await import('./log-rotation');
96
- startLogRotation(() => getAllProcesses());
97
-
98
- // Start sticky port checker - periodically try original port if we're on a fallback
99
- if (resolvedPort !== requestedPort) {
100
- startStickyPortChecker();
101
- }
102
- }
103
-
104
- function startStickyPortChecker() {
105
- const CHECK_INTERVAL_MS = 60_000; // Check every 60 seconds
106
- console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
107
-
108
- setInterval(async () => {
109
- if (_currentPort === _originalPort) return; // Already on original port
110
-
111
- try {
112
- const proc = Bun.spawn(['powershell', '-NoProfile', '-Command',
113
- `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
114
- ], { stdout: 'pipe', stderr: 'pipe' });
115
- const text = await new Response(proc.stdout).text();
116
- const pid = parseInt(text.trim(), 10);
117
-
118
- if (!pid) {
119
- console.log(`[server] ✓ Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
120
- _currentPort = _originalPort;
121
- }
122
- } catch { /* best-effort */ }
123
- }, CHECK_INTERVAL_MS);
124
- }
125
-
126
- /**
127
- * Built-in Process Guard
128
- *
129
- * Runs as a background loop inside the dashboard process.
130
- * Every GUARD_INTERVAL_MS, checks processes with BGR_KEEP_ALIVE=true
131
- * in their env and auto-restarts any that died.
132
- *
133
- * Only guarded processes (opted-in via dashboard toggle or env var) are
134
- * monitored. Other processes are left alone even if they crash.
135
- *
136
- * Toggle guard per-process:
137
- * - Dashboard UI: click the shield icon on any process row
138
- * - CLI: set BGR_KEEP_ALIVE=true in the process env/config
139
- */
140
- function startGuard() {
141
- console.log(`[guard] ✓ Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
142
-
143
- setInterval(async () => {
144
- try {
145
- const processes = getAllProcesses();
146
- if (processes.length === 0) return;
147
-
148
- for (const proc of processes) {
149
- // Skip the dashboard itself
150
- if (GUARD_SKIP_NAMES.has(proc.name)) continue;
151
-
152
- // Only guard processes with BGR_KEEP_ALIVE=true
153
- const env = proc.env ? parseEnvString(proc.env) : {};
154
- if (env.BGR_KEEP_ALIVE !== 'true') continue;
155
-
156
- const alive = await isProcessRunning(proc.pid, proc.command);
157
- if (!alive) {
158
- const now = Date.now();
159
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
160
- if (now < nextRestart) continue; // Still in backoff period
161
-
162
- console.log(`[guard] ⚠ Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
163
- let success = false;
164
- try {
165
- await handleRun({
166
- action: 'run',
167
- name: proc.name,
168
- force: true,
169
- remoteName: '',
170
- });
171
- success = true;
172
-
173
- // Track restart count
174
- const prevCount = guardRestartCounts.get(proc.name) || 0;
175
- const newCount = prevCount + 1;
176
- guardRestartCounts.set(proc.name, newCount);
177
-
178
- // Record in history database
179
- try {
180
- addHistoryEntry(proc.name, 'restart', undefined, { by: 'guard', count: newCount });
181
- } catch { /* ignore history errors */ }
182
-
183
- // Record event for dashboard
184
- guardEvents.unshift({ time: now, name: proc.name, action: 'restart', success: true });
185
- if (guardEvents.length > 100) guardEvents.pop();
186
-
187
- // Exponential backoff if it crashes repeatedly (more than 5 times)
188
- if (newCount > 5) {
189
- const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300); // 30s, 60s, 120s, up to 5 mins
190
- guardNextRestartTime.set(proc.name, Date.now() + (backoffSeconds * 1000));
191
- console.log(`[guard] ✓ Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
192
- } else {
193
- console.log(`[guard] ✓ Restarted "${proc.name}" (restart #${newCount})`);
194
- }
195
- } catch (err: any) {
196
- console.error(`[guard] ✗ Failed to restart "${proc.name}": ${err.message}`);
197
- guardEvents.unshift({ time: now, name: proc.name, action: 'restart', success: false });
198
- if (guardEvents.length > 100) guardEvents.pop();
199
- }
200
- } else {
201
- // Reset counter if process has been stable (alive at least once during check)
202
- const prevCount = guardRestartCounts.get(proc.name) || 0;
203
- if (prevCount > 0) {
204
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
205
- if (Date.now() > nextRestart + 60_000) {
206
- // If it lived over 60s past its backoff threshold, consider it stable
207
- guardRestartCounts.delete(proc.name);
208
- guardNextRestartTime.delete(proc.name);
209
- }
210
- }
211
- }
212
- }
213
- } catch (err: any) {
214
- console.error(`[guard] Error in guard loop: ${err.message}`);
215
- }
216
- }, GUARD_INTERVAL_MS);
217
- }
package/src/table.ts DELETED
@@ -1,232 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import chalk from "chalk";
4
-
5
- export interface TableColumn {
6
- key: string;
7
- header: string;
8
- formatter?: (value: any) => string;
9
- truncator?: (value: string, maxLength: number) => string;
10
- }
11
-
12
- export interface TableOptions {
13
- maxWidth?: number;
14
- padding?: number;
15
- borderStyle?: "rounded" | "single" | "double" | "none";
16
- showHeaders?: boolean;
17
- }
18
-
19
- export interface ProcessTableRow {
20
- id: number;
21
- pid: number;
22
- name: string;
23
- port: string;
24
- memory: string;
25
- command: string;
26
- workdir: string;
27
- status: string;
28
- runtime: string;
29
- }
30
-
31
- // Get terminal width or use default
32
- export function getTerminalWidth(): number {
33
- return process.stdout.columns || 120;
34
- }
35
-
36
- // Strip ANSI color codes for accurate length calculation
37
- export function stripAnsi(str: string): string {
38
- return str.replace(/\u001b\[[0-9;]*m/g, "");
39
- }
40
-
41
- // Default truncator: trims the end of a string
42
- export function truncateString(str: string, maxLength: number): string {
43
- const stripped = stripAnsi(str);
44
- if (stripped.length <= maxLength) return str;
45
- const ellipsis = "…";
46
- // Ensure maxLength is at least 1 for the ellipsis
47
- if (maxLength < 1) return "";
48
- if (maxLength === 1) return ellipsis;
49
-
50
- const targetLength = maxLength - ellipsis.length;
51
- return str.substring(0, targetLength > 0 ? targetLength : 0) + ellipsis;
52
- }
53
-
54
- // Path truncator: trims the middle of a string
55
- export function truncatePath(str: string, maxLength: number): string {
56
- const stripped = stripAnsi(str);
57
- if (stripped.length <= maxLength) return str;
58
- const ellipsis = "…";
59
- // Ensure maxLength is at least 3 for a start, middle, and end character
60
- if (maxLength < 3) return truncateString(str, maxLength);
61
-
62
- const targetLength = maxLength - ellipsis.length;
63
- const startLen = Math.ceil(targetLength / 2);
64
- const endLen = Math.floor(targetLength / 2);
65
- return str.substring(0, startLen) + ellipsis + str.substring(str.length - endLen);
66
- }
67
-
68
- // Calculate column widths by proportionally shrinking the widest columns
69
- export function calculateColumnWidths(
70
- rows: any[],
71
- columns: TableColumn[],
72
- maxWidth: number,
73
- padding: number = 2
74
- ): Map<string, number> {
75
- const separatorsWidth = columns.length + 1;
76
- const paddingWidth = padding * columns.length;
77
- const availableWidth = maxWidth - separatorsWidth - paddingWidth;
78
-
79
- const naturalWidths = new Map<string, number>();
80
-
81
- // 1. Calculate the natural width (max content length) for each column
82
- for (const col of columns) {
83
- let maxNatural = stripAnsi(col.header).length;
84
- for (const row of rows) {
85
- const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
86
- maxNatural = Math.max(maxNatural, stripAnsi(value).length);
87
- }
88
- naturalWidths.set(col.key, maxNatural);
89
- }
90
-
91
- const totalNaturalWidth = Array.from(naturalWidths.values()).reduce((sum, w) => sum + w, 0);
92
-
93
- // 2. If it fits, we're done
94
- if (totalNaturalWidth <= availableWidth) {
95
- return naturalWidths;
96
- }
97
-
98
- // 3. If not, calculate the overage and shrink the widest columns iteratively
99
- let overage = totalNaturalWidth - availableWidth;
100
- const currentWidths = new Map(naturalWidths);
101
-
102
- while (overage > 0) {
103
- // Find the column that is currently the widest
104
- let widestColKey: string | null = null;
105
- let maxW = -1;
106
- for (const [key, width] of currentWidths.entries()) {
107
- if (width > maxW) {
108
- maxW = width;
109
- widestColKey = key;
110
- }
111
- }
112
-
113
- // If no column can be shrunk (e.g., all are width 0), break
114
- if (widestColKey === null || maxW <= 1) {
115
- break;
116
- }
117
-
118
- // Shrink the widest column by 1
119
- currentWidths.set(widestColKey, maxW - 1);
120
- overage--;
121
- }
122
-
123
- return currentWidths;
124
- }
125
-
126
- function renderBorder(widths: number[], padding: number, style: string[]): string {
127
- const [left, mid, right, line] = style;
128
- let lineStr = left;
129
- for (let i = 0; i < widths.length; i++) {
130
- lineStr += line.repeat(widths[i] + padding);
131
- if (i < widths.length - 1) {
132
- lineStr += mid;
133
- }
134
- }
135
- lineStr += right;
136
- return lineStr;
137
- }
138
-
139
- export function renderHorizontalTable(
140
- rows: any[],
141
- columns: TableColumn[],
142
- options: TableOptions = {}
143
- ): { table: string; truncatedIndices: number[] } {
144
- const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
145
- if (rows.length === 0) return { table: chalk.gray("No data to display"), truncatedIndices: [] };
146
-
147
- const borderChars: Record<string, string[]> = {
148
- rounded: ["╭", "┬", "╮", "─", "│", "├", "┼", "┤", "╰", "┴", "╯"],
149
- single: ["┌", "┬", "┐", "─", "│", "├", "┼", "┤", "└", "┴", "┘"],
150
- double: ["╔", "╦", "╗", "═", "║", "╠", "╬", "╣", "╚", "╩", "╝"],
151
- none: [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
152
- };
153
- const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars[borderStyle] ?? borderChars.rounded;
154
- const columnWidths = calculateColumnWidths(rows, columns, maxWidth, padding);
155
- const widthArray = columns.map((col) => columnWidths.get(col.key)!);
156
- const truncatedIndices = new Set<number>();
157
- const lines: string[] = [];
158
- const cellPadding = " ".repeat(padding / 2);
159
-
160
- if (borderStyle !== "none") lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
161
-
162
- if (showHeaders) {
163
- const headerCells = columns.map((col, i) => chalk.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
164
- lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
165
- if (borderStyle !== "none") lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
166
- }
167
-
168
- rows.forEach((row, rowIndex) => {
169
- const cells = columns.map((col, i) => {
170
- const width = widthArray[i];
171
- const originalValue = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
172
- if (stripAnsi(originalValue).length > width) {
173
- truncatedIndices.add(rowIndex);
174
- }
175
- const truncator = col.truncator || truncateString;
176
- const truncated = truncator(originalValue, width);
177
- return truncated + " ".repeat(Math.max(0, width - stripAnsi(truncated).length));
178
- });
179
- lines.push(`${v}${cellPadding}${cells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
180
- });
181
-
182
- if (borderStyle !== "none") lines.push(renderBorder(widthArray, padding, [bl, bc, br, h]));
183
-
184
- return { table: lines.join("\n"), truncatedIndices: Array.from(truncatedIndices) };
185
- }
186
-
187
- export function renderVerticalTree(rows: any[], columns: TableColumn[]): string {
188
- const lines: string[] = [];
189
- rows.forEach((row, index) => {
190
- if (index > 0) lines.push("");
191
- const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
192
- lines.push(chalk.cyan(`▶ ${name}`));
193
-
194
- columns.forEach((col) => {
195
- const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
196
- lines.push(` ├─ ${chalk.gray(`${col.header}:`)} ${value}`);
197
- });
198
- });
199
- return lines.join("\n");
200
- }
201
-
202
- export function renderHybridTable(
203
- rows: any[],
204
- columns: TableColumn[],
205
- options: TableOptions = {}
206
- ): string {
207
- const { table, truncatedIndices } = renderHorizontalTable(rows, columns, options);
208
- const output = [table];
209
-
210
- if (truncatedIndices.length > 0) {
211
- const truncatedRows = truncatedIndices.map((i) => rows[i]);
212
- output.push("\n" + renderVerticalTree(truncatedRows, columns));
213
- }
214
-
215
- return output.join("\n");
216
- }
217
-
218
- export function renderProcessTable(processes: ProcessTableRow[], options?: TableOptions): string {
219
- const columns: TableColumn[] = [
220
- { key: "id", header: "ID", formatter: (id) => chalk.blue(id) },
221
- { key: "pid", header: "PID", formatter: (pid) => chalk.yellow(pid) },
222
- { key: "name", header: "Name", formatter: (name) => chalk.cyan.bold(name) },
223
- { key: "port", header: "Port", formatter: (port) => port === '-' ? chalk.gray(port) : chalk.hex('#FF6B6B')(port) },
224
- { key: "memory", header: "Memory", formatter: (mem) => mem === '-' ? chalk.gray(mem) : chalk.hex('#4ECDC4')(mem) },
225
- { key: "command", header: "Command" },
226
- { key: "workdir", header: "Directory", formatter: (dir) => chalk.gray(dir), truncator: truncatePath },
227
- { key: "status", header: "Status" },
228
- { key: "runtime", header: "Runtime", formatter: (runtime) => chalk.magenta(runtime) },
229
- ];
230
-
231
- return renderHybridTable(processes, columns, options);
232
- }
package/src/types.ts DELETED
@@ -1,14 +0,0 @@
1
- export interface CommandOptions {
2
- remoteName?: string;
3
- command?: string;
4
- directory?: string;
5
- env?: Record<string, string>;
6
- configPath?: string;
7
- action?: string;
8
- name?: string;
9
- force?: boolean;
10
- fetch?: boolean;
11
- stdout?: string;
12
- stderr?: string;
13
- dbPath?: string;
14
- }
package/src/utils.ts DELETED
@@ -1,96 +0,0 @@
1
-
2
- export function parseEnvString(envString: string): Record<string, string> {
3
- const env: Record<string, string> = {};
4
- envString.split(",").forEach(pair => {
5
- const [key, value] = pair.split("=");
6
- if (key && value) env[key] = value;
7
- });
8
- return env;
9
- }
10
-
11
-
12
- export function calculateRuntime(startTime: string): string {
13
- const start = new Date(startTime).getTime();
14
- const now = new Date().getTime();
15
- const diffInMinutes = Math.floor((now - start) / (1000 * 60));
16
- return `${diffInMinutes} minutes`;
17
- }
18
-
19
- // Re-export platform utils for backward compatibility and convenience
20
- export { isProcessRunning } from "./platform";
21
-
22
- import * as fs from "fs";
23
- import chalk from "chalk";
24
-
25
- // Read version at runtime instead of using macros (macros crash on Windows)
26
- export async function getVersion(): Promise<string> {
27
- try {
28
- const { join } = await import("path");
29
- const pkgPath = join(import.meta.dir, '../package.json');
30
- const pkg = await Bun.file(pkgPath).json();
31
- return pkg.version || '0.0.0';
32
- } catch {
33
- return '0.0.0';
34
- }
35
- }
36
-
37
-
38
- export function validateDirectory(directory: string) {
39
- if (!directory || !fs.existsSync(directory)) {
40
- // Throw instead of process.exit() — lets dashboard API handlers catch gracefully
41
- throw new Error(`Directory not found or invalid: '${directory}'`);
42
- }
43
- }
44
-
45
- export function tailFile(path: string, prefix: string, colorFn: (s: string) => string, lines?: number): () => void {
46
- let position = 0;
47
- let lastPartial = '';
48
-
49
- if (!fs.existsSync(path)) {
50
- return () => { };
51
- }
52
-
53
- const fd = fs.openSync(path, 'r');
54
-
55
- const printNewContent = () => {
56
- try {
57
- const stats = fs.statSync(path);
58
- if (stats.size <= position) return;
59
-
60
- const buffer = Buffer.alloc(stats.size - position);
61
- fs.readSync(fd, buffer, 0, buffer.length, position);
62
-
63
- let content = buffer.toString();
64
- content = lastPartial + content;
65
- lastPartial = '';
66
-
67
- const lineArray = content.split(/\r?\n/);
68
- if (!content.endsWith('\n')) {
69
- lastPartial = lineArray.pop() || '';
70
- }
71
-
72
- lineArray.forEach(line => {
73
- if (line) {
74
- console.log(colorFn(prefix + line));
75
- }
76
- });
77
-
78
- position = stats.size;
79
- } catch (e) {
80
- // ignore read errors
81
- }
82
- };
83
-
84
- const watcher = fs.watch(path, { persistent: true }, (event) => {
85
- if (event === 'change') {
86
- printNewContent();
87
- }
88
- });
89
-
90
- printNewContent(); // Check immediately
91
-
92
- return () => {
93
- watcher.close();
94
- try { fs.closeSync(fd); } catch { }
95
- };
96
- }