bgrun 3.12.12 → 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.
- package/README.md +2 -2
- package/dashboard/lib/runtime.ts +49 -0
- package/package.json +2 -17
- package/src/api.ts +0 -116
- package/src/build.ts +0 -31
- package/src/commands/cleanup.ts +0 -141
- package/src/commands/details.ts +0 -60
- package/src/commands/list.ts +0 -133
- package/src/commands/logs.ts +0 -49
- package/src/commands/run.ts +0 -217
- package/src/commands/watch.ts +0 -223
- package/src/config.ts +0 -37
- package/src/db.ts +0 -422
- package/src/deploy.ts +0 -163
- package/src/deps.ts +0 -126
- package/src/guard.ts +0 -208
- package/src/index.ts +0 -623
- package/src/log-rotation.ts +0 -93
- package/src/logger.ts +0 -40
- package/src/platform.ts +0 -665
- package/src/server.ts +0 -217
- package/src/table.ts +0 -232
- package/src/types.ts +0 -14
- 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
|
-
}
|