bgrun 3.3.1
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 +720 -0
- package/dashboard/app/api/logs/[name]/route.ts +17 -0
- package/dashboard/app/api/processes/[name]/route.ts +19 -0
- package/dashboard/app/api/processes/route.ts +150 -0
- package/dashboard/app/api/restart/[name]/route.ts +20 -0
- package/dashboard/app/api/start/route.ts +22 -0
- package/dashboard/app/api/stop/[name]/route.ts +16 -0
- package/dashboard/app/api/version/route.ts +8 -0
- package/dashboard/app/globals.css +1135 -0
- package/dashboard/app/layout.tsx +47 -0
- package/dashboard/app/page.client.tsx +554 -0
- package/dashboard/app/page.tsx +130 -0
- package/dist/index.js +1580 -0
- package/examples/bgr-startup.sh +40 -0
- package/package.json +60 -0
- package/src/api.ts +31 -0
- package/src/build.ts +26 -0
- package/src/commands/cleanup.ts +142 -0
- package/src/commands/details.ts +46 -0
- package/src/commands/list.ts +86 -0
- package/src/commands/logs.ts +49 -0
- package/src/commands/run.ts +151 -0
- package/src/commands/watch.ts +223 -0
- package/src/config.ts +37 -0
- package/src/db.ts +115 -0
- package/src/index.ts +349 -0
- package/src/logger.ts +29 -0
- package/src/platform.ts +440 -0
- package/src/schema.ts +2 -0
- package/src/server.ts +24 -0
- package/src/table.ts +230 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +99 -0
- package/src/version.macro.ts +17 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
+
var __require = import.meta.require;
|
|
15
|
+
|
|
16
|
+
// src/platform.ts
|
|
17
|
+
var exports_platform = {};
|
|
18
|
+
__export(exports_platform, {
|
|
19
|
+
waitForPortFree: () => waitForPortFree,
|
|
20
|
+
terminateProcess: () => terminateProcess,
|
|
21
|
+
readFileTail: () => readFileTail,
|
|
22
|
+
killProcessOnPort: () => killProcessOnPort,
|
|
23
|
+
isWindows: () => isWindows,
|
|
24
|
+
isProcessRunning: () => isProcessRunning,
|
|
25
|
+
isPortFree: () => isPortFree,
|
|
26
|
+
getShellCommand: () => getShellCommand,
|
|
27
|
+
getProcessPorts: () => getProcessPorts,
|
|
28
|
+
getProcessMemory: () => getProcessMemory,
|
|
29
|
+
getHomeDir: () => getHomeDir,
|
|
30
|
+
findPidByPort: () => findPidByPort,
|
|
31
|
+
findChildPid: () => findChildPid,
|
|
32
|
+
ensureDir: () => ensureDir,
|
|
33
|
+
copyFile: () => copyFile
|
|
34
|
+
});
|
|
35
|
+
import * as fs from "fs";
|
|
36
|
+
import * as os from "os";
|
|
37
|
+
var {$ } = globalThis.Bun;
|
|
38
|
+
function isWindows() {
|
|
39
|
+
return process.platform === "win32";
|
|
40
|
+
}
|
|
41
|
+
function getHomeDir() {
|
|
42
|
+
return os.homedir();
|
|
43
|
+
}
|
|
44
|
+
async function isProcessRunning(pid, command) {
|
|
45
|
+
try {
|
|
46
|
+
if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
|
|
47
|
+
return await isDockerContainerRunning(command);
|
|
48
|
+
}
|
|
49
|
+
if (isWindows()) {
|
|
50
|
+
const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
|
|
51
|
+
return result.includes(`${pid}`);
|
|
52
|
+
} else {
|
|
53
|
+
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
54
|
+
return result.includes(`${pid}`);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function isDockerContainerRunning(command) {
|
|
61
|
+
try {
|
|
62
|
+
const nameMatch = command.match(/--name\s+["']?(\S+?)["']?(?:\s|$)/);
|
|
63
|
+
if (nameMatch) {
|
|
64
|
+
const containerName = nameMatch[1];
|
|
65
|
+
const result = await $`docker inspect -f "{{.State.Running}}" ${containerName}`.nothrow().text();
|
|
66
|
+
return result.trim() === "true";
|
|
67
|
+
}
|
|
68
|
+
const imageMatch = command.match(/docker\s+run\s+.*?(?:-d\s+)?(\S+)\s*$/);
|
|
69
|
+
if (imageMatch) {
|
|
70
|
+
const imageName = imageMatch[1];
|
|
71
|
+
const result = await $`docker ps --filter ancestor=${imageName} --format "{{.ID}}"`.nothrow().text();
|
|
72
|
+
return result.trim().length > 0;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function getChildPids(pid) {
|
|
80
|
+
try {
|
|
81
|
+
if (isWindows()) {
|
|
82
|
+
const result = await $`wmic process where (ParentProcessId=${pid}) get ProcessId`.nothrow().text();
|
|
83
|
+
return result.split(`
|
|
84
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
85
|
+
} else {
|
|
86
|
+
const result = await $`ps --no-headers -o pid --ppid ${pid}`.nothrow().text();
|
|
87
|
+
return result.trim().split(`
|
|
88
|
+
`).filter((p) => p.trim()).map((p) => parseInt(p)).filter((n) => !isNaN(n));
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function terminateProcess(pid, force = false) {
|
|
95
|
+
const children = await getChildPids(pid);
|
|
96
|
+
for (const childPid of children) {
|
|
97
|
+
try {
|
|
98
|
+
if (isWindows()) {
|
|
99
|
+
if (force) {
|
|
100
|
+
await $`taskkill /F /PID ${childPid}`.nothrow().quiet();
|
|
101
|
+
} else {
|
|
102
|
+
await $`taskkill /PID ${childPid}`.nothrow().quiet();
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
const signal = force ? "KILL" : "TERM";
|
|
106
|
+
await $`kill -${signal} ${childPid}`.nothrow();
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
await Bun.sleep(500);
|
|
111
|
+
if (await isProcessRunning(pid)) {
|
|
112
|
+
try {
|
|
113
|
+
if (isWindows()) {
|
|
114
|
+
if (force) {
|
|
115
|
+
await $`taskkill /F /PID ${pid}`.nothrow().quiet();
|
|
116
|
+
} else {
|
|
117
|
+
await $`taskkill /PID ${pid}`.nothrow().quiet();
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
const signal = force ? "KILL" : "TERM";
|
|
121
|
+
await $`kill -${signal} ${pid}`.nothrow();
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function isPortFree(port) {
|
|
127
|
+
try {
|
|
128
|
+
if (isWindows()) {
|
|
129
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
130
|
+
for (const line of result.split(`
|
|
131
|
+
`)) {
|
|
132
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING`));
|
|
133
|
+
if (match)
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true;
|
|
137
|
+
} else {
|
|
138
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
139
|
+
const lines = result.trim().split(`
|
|
140
|
+
`).filter((l) => l.trim());
|
|
141
|
+
return lines.length <= 1;
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
148
|
+
const startTime = Date.now();
|
|
149
|
+
const pollInterval = 300;
|
|
150
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
151
|
+
if (await isPortFree(port)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
await Bun.sleep(pollInterval);
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
async function killProcessOnPort(port) {
|
|
159
|
+
try {
|
|
160
|
+
if (isWindows()) {
|
|
161
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
162
|
+
const pids = new Set;
|
|
163
|
+
for (const line of result.split(`
|
|
164
|
+
`)) {
|
|
165
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*?\\s+(\\d+)\\s*$`));
|
|
166
|
+
if (match && parseInt(match[1]) === port) {
|
|
167
|
+
const pid = parseInt(match[2]);
|
|
168
|
+
if (pid > 0)
|
|
169
|
+
pids.add(pid);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const pid of pids) {
|
|
173
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
174
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
const result = await $`lsof -ti :${port}`.nothrow().text();
|
|
178
|
+
if (result.trim()) {
|
|
179
|
+
const pids = result.trim().split(`
|
|
180
|
+
`).filter((pid) => pid);
|
|
181
|
+
for (const pid of pids) {
|
|
182
|
+
await $`kill -9 ${pid}`.nothrow();
|
|
183
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.warn(`Warning: Could not check or kill process on port ${port}: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function ensureDir(dirPath) {
|
|
192
|
+
if (!fs.existsSync(dirPath)) {
|
|
193
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function getShellCommand(command) {
|
|
197
|
+
if (isWindows()) {
|
|
198
|
+
return ["cmd", "/c", command];
|
|
199
|
+
} else {
|
|
200
|
+
return ["sh", "-c", command];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function findChildPid(parentPid) {
|
|
204
|
+
let currentPid = parentPid;
|
|
205
|
+
const maxDepth = 5;
|
|
206
|
+
for (let depth = 0;depth < maxDepth; depth++) {
|
|
207
|
+
try {
|
|
208
|
+
let childPids = [];
|
|
209
|
+
if (isWindows()) {
|
|
210
|
+
const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
|
|
211
|
+
childPids = result.split(`
|
|
212
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
213
|
+
} else {
|
|
214
|
+
const result = await $`ps --no-headers -o pid --ppid ${currentPid}`.nothrow().text();
|
|
215
|
+
childPids = result.trim().split(`
|
|
216
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
217
|
+
}
|
|
218
|
+
if (childPids.length === 0) {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
currentPid = childPids[0];
|
|
222
|
+
} catch {
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return currentPid;
|
|
227
|
+
}
|
|
228
|
+
async function findPidByPort(port, maxWaitMs = 8000) {
|
|
229
|
+
const start = Date.now();
|
|
230
|
+
const pollMs = 500;
|
|
231
|
+
while (Date.now() - start < maxWaitMs) {
|
|
232
|
+
try {
|
|
233
|
+
if (isWindows()) {
|
|
234
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
235
|
+
for (const line of result.split(`
|
|
236
|
+
`)) {
|
|
237
|
+
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
238
|
+
const parts = line.trim().split(/\s+/);
|
|
239
|
+
const pid = parseInt(parts[parts.length - 1]);
|
|
240
|
+
if (!isNaN(pid) && pid > 0)
|
|
241
|
+
return pid;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
try {
|
|
246
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
247
|
+
for (const line of result2.split(`
|
|
248
|
+
`)) {
|
|
249
|
+
if (line.includes(`:${port}`)) {
|
|
250
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
251
|
+
if (pidMatch)
|
|
252
|
+
return parseInt(pidMatch[1]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch {}
|
|
256
|
+
const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
|
|
257
|
+
const pid = parseInt(result.trim());
|
|
258
|
+
if (!isNaN(pid) && pid > 0)
|
|
259
|
+
return pid;
|
|
260
|
+
}
|
|
261
|
+
} catch {}
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
async function readFileTail(filePath, lines) {
|
|
267
|
+
try {
|
|
268
|
+
const content = await Bun.file(filePath).text();
|
|
269
|
+
if (!lines) {
|
|
270
|
+
return content;
|
|
271
|
+
}
|
|
272
|
+
const allLines = content.split(/\r?\n/);
|
|
273
|
+
const tailLines = allLines.slice(-lines);
|
|
274
|
+
return tailLines.join(`
|
|
275
|
+
`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
throw new Error(`Error reading file: ${error}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function copyFile(src, dest) {
|
|
281
|
+
fs.copyFileSync(src, dest);
|
|
282
|
+
}
|
|
283
|
+
async function getProcessMemory(pid) {
|
|
284
|
+
try {
|
|
285
|
+
if (isWindows()) {
|
|
286
|
+
const result = await $`wmic process where ProcessId=${pid} get WorkingSetSize`.nothrow().text();
|
|
287
|
+
const lines = result.split(`
|
|
288
|
+
`).filter((line) => line.trim() && !line.includes("WorkingSetSize"));
|
|
289
|
+
if (lines.length > 0) {
|
|
290
|
+
return parseInt(lines[0].trim()) || 0;
|
|
291
|
+
}
|
|
292
|
+
return 0;
|
|
293
|
+
} else {
|
|
294
|
+
const result = await $`ps -o rss= -p ${pid}`.text();
|
|
295
|
+
const memoryKB = parseInt(result.trim());
|
|
296
|
+
return memoryKB * 1024;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
return 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function getProcessPorts(pid) {
|
|
303
|
+
try {
|
|
304
|
+
if (isWindows()) {
|
|
305
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
306
|
+
const ports = new Set;
|
|
307
|
+
for (const line of result.split(`
|
|
308
|
+
`)) {
|
|
309
|
+
const match = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/);
|
|
310
|
+
if (match && parseInt(match[2]) === pid) {
|
|
311
|
+
ports.add(parseInt(match[1]));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return Array.from(ports);
|
|
315
|
+
} else {
|
|
316
|
+
try {
|
|
317
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
318
|
+
const ports2 = new Set;
|
|
319
|
+
for (const line of result2.split(`
|
|
320
|
+
`)) {
|
|
321
|
+
if (line.includes(`pid=${pid}`)) {
|
|
322
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
323
|
+
if (portMatch) {
|
|
324
|
+
ports2.add(parseInt(portMatch[1]));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (ports2.size > 0)
|
|
329
|
+
return Array.from(ports2);
|
|
330
|
+
} catch {}
|
|
331
|
+
const result = await $`lsof -i -P -n -p ${pid}`.nothrow().quiet().text();
|
|
332
|
+
const ports = new Set;
|
|
333
|
+
for (const line of result.split(`
|
|
334
|
+
`)) {
|
|
335
|
+
if (line.includes("LISTEN")) {
|
|
336
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
337
|
+
if (portMatch) {
|
|
338
|
+
ports.add(parseInt(portMatch[1]));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return Array.from(ports);
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var init_platform = () => {};
|
|
349
|
+
|
|
350
|
+
// src/index.ts
|
|
351
|
+
import { parseArgs } from "util";
|
|
352
|
+
|
|
353
|
+
// src/utils.ts
|
|
354
|
+
init_platform();
|
|
355
|
+
import * as fs2 from "fs";
|
|
356
|
+
import { join } from "path";
|
|
357
|
+
import chalk from "chalk";
|
|
358
|
+
function parseEnvString(envString) {
|
|
359
|
+
const env = {};
|
|
360
|
+
envString.split(",").forEach((pair) => {
|
|
361
|
+
const [key, value] = pair.split("=");
|
|
362
|
+
if (key && value)
|
|
363
|
+
env[key] = value;
|
|
364
|
+
});
|
|
365
|
+
return env;
|
|
366
|
+
}
|
|
367
|
+
function calculateRuntime(startTime) {
|
|
368
|
+
const start = new Date(startTime).getTime();
|
|
369
|
+
const now = new Date().getTime();
|
|
370
|
+
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
371
|
+
return `${diffInMinutes} minutes`;
|
|
372
|
+
}
|
|
373
|
+
async function getVersion() {
|
|
374
|
+
try {
|
|
375
|
+
const pkgPath = join(import.meta.dir, "../package.json");
|
|
376
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
377
|
+
return pkg.version || "0.0.0";
|
|
378
|
+
} catch {
|
|
379
|
+
return "0.0.0";
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function validateDirectory(directory) {
|
|
383
|
+
if (!directory || !fs2.existsSync(directory)) {
|
|
384
|
+
console.log(chalk.red("\u274C Error: 'directory' must be a valid path."));
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function tailFile(path, prefix, colorFn, lines) {
|
|
389
|
+
let position = 0;
|
|
390
|
+
let lastPartial = "";
|
|
391
|
+
if (!fs2.existsSync(path)) {
|
|
392
|
+
return () => {};
|
|
393
|
+
}
|
|
394
|
+
const fd = fs2.openSync(path, "r");
|
|
395
|
+
const printNewContent = () => {
|
|
396
|
+
try {
|
|
397
|
+
const stats = fs2.statSync(path);
|
|
398
|
+
if (stats.size <= position)
|
|
399
|
+
return;
|
|
400
|
+
const buffer = Buffer.alloc(stats.size - position);
|
|
401
|
+
fs2.readSync(fd, buffer, 0, buffer.length, position);
|
|
402
|
+
let content = buffer.toString();
|
|
403
|
+
content = lastPartial + content;
|
|
404
|
+
lastPartial = "";
|
|
405
|
+
const lineArray = content.split(/\r?\n/);
|
|
406
|
+
if (!content.endsWith(`
|
|
407
|
+
`)) {
|
|
408
|
+
lastPartial = lineArray.pop() || "";
|
|
409
|
+
}
|
|
410
|
+
lineArray.forEach((line) => {
|
|
411
|
+
if (line) {
|
|
412
|
+
console.log(colorFn(prefix + line));
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
position = stats.size;
|
|
416
|
+
} catch (e) {}
|
|
417
|
+
};
|
|
418
|
+
const watcher = fs2.watch(path, { persistent: true }, (event) => {
|
|
419
|
+
if (event === "change") {
|
|
420
|
+
printNewContent();
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
printNewContent();
|
|
424
|
+
return () => {
|
|
425
|
+
watcher.close();
|
|
426
|
+
try {
|
|
427
|
+
fs2.closeSync(fd);
|
|
428
|
+
} catch {}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/db.ts
|
|
433
|
+
init_platform();
|
|
434
|
+
import { Database, z } from "sqlite-zod-orm";
|
|
435
|
+
import { join as join2 } from "path";
|
|
436
|
+
var {sleep } = globalThis.Bun;
|
|
437
|
+
var ProcessSchema = z.object({
|
|
438
|
+
pid: z.number(),
|
|
439
|
+
workdir: z.string(),
|
|
440
|
+
command: z.string(),
|
|
441
|
+
name: z.string(),
|
|
442
|
+
env: z.string(),
|
|
443
|
+
configPath: z.string().default(""),
|
|
444
|
+
stdout_path: z.string(),
|
|
445
|
+
stderr_path: z.string(),
|
|
446
|
+
timestamp: z.string().default(() => new Date().toISOString())
|
|
447
|
+
});
|
|
448
|
+
var homePath = getHomeDir();
|
|
449
|
+
var dbName = process.env.DB_NAME ?? "bgr";
|
|
450
|
+
var dbPath = join2(homePath, ".bgr", `${dbName}_v2.sqlite`);
|
|
451
|
+
ensureDir(join2(homePath, ".bgr"));
|
|
452
|
+
var db = new Database(dbPath, {
|
|
453
|
+
process: ProcessSchema
|
|
454
|
+
}, {
|
|
455
|
+
indexes: {
|
|
456
|
+
process: ["name", "timestamp", "pid"]
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
function getProcess(name) {
|
|
460
|
+
return db.process.select().where({ name }).orderBy("timestamp", "desc").limit(1).get() || null;
|
|
461
|
+
}
|
|
462
|
+
function getAllProcesses() {
|
|
463
|
+
return db.process.select().all();
|
|
464
|
+
}
|
|
465
|
+
function insertProcess(data) {
|
|
466
|
+
return db.process.insert({
|
|
467
|
+
...data,
|
|
468
|
+
timestamp: new Date().toISOString()
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
function removeProcess(pid) {
|
|
472
|
+
const matches = db.process.select().where({ pid }).all();
|
|
473
|
+
for (const p of matches) {
|
|
474
|
+
db.process.delete(p.id);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function removeProcessByName(name) {
|
|
478
|
+
const matches = db.process.select().where({ name }).all();
|
|
479
|
+
for (const p of matches) {
|
|
480
|
+
db.process.delete(p.id);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function removeAllProcesses() {
|
|
484
|
+
const all = db.process.select().all();
|
|
485
|
+
for (const p of all) {
|
|
486
|
+
db.process.delete(p.id);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
490
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
491
|
+
try {
|
|
492
|
+
return operation();
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (err?.code === "SQLITE_BUSY" && attempt < maxRetries) {
|
|
495
|
+
await sleep(delay * attempt);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
throw new Error("Max retries reached for database operation");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/commands/run.ts
|
|
505
|
+
init_platform();
|
|
506
|
+
|
|
507
|
+
// src/logger.ts
|
|
508
|
+
import boxen from "boxen";
|
|
509
|
+
import chalk2 from "chalk";
|
|
510
|
+
function announce(message, title) {
|
|
511
|
+
console.log(boxen(chalk2.white(message), {
|
|
512
|
+
padding: 1,
|
|
513
|
+
margin: 1,
|
|
514
|
+
borderColor: "green",
|
|
515
|
+
title: title || "bgrun",
|
|
516
|
+
titleAlignment: "center",
|
|
517
|
+
borderStyle: "round"
|
|
518
|
+
}));
|
|
519
|
+
}
|
|
520
|
+
function error(message) {
|
|
521
|
+
console.error(boxen(chalk2.red(message), {
|
|
522
|
+
padding: 1,
|
|
523
|
+
margin: 1,
|
|
524
|
+
borderColor: "red",
|
|
525
|
+
title: "Error",
|
|
526
|
+
titleAlignment: "center",
|
|
527
|
+
borderStyle: "double"
|
|
528
|
+
}));
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/config.ts
|
|
533
|
+
function formatEnvKey(key) {
|
|
534
|
+
return key.toUpperCase().replace(/\./g, "_");
|
|
535
|
+
}
|
|
536
|
+
function flattenConfig(obj, prefix = "") {
|
|
537
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
538
|
+
const value = obj[key];
|
|
539
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
540
|
+
if (Array.isArray(value)) {
|
|
541
|
+
value.forEach((item, index) => {
|
|
542
|
+
const indexedPrefix = `${newPrefix}.${index}`;
|
|
543
|
+
if (typeof item === "object" && item !== null) {
|
|
544
|
+
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
545
|
+
} else {
|
|
546
|
+
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
} else if (typeof value === "object" && value !== null) {
|
|
550
|
+
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
551
|
+
} else {
|
|
552
|
+
acc[formatEnvKey(newPrefix)] = String(value);
|
|
553
|
+
}
|
|
554
|
+
return acc;
|
|
555
|
+
}, {});
|
|
556
|
+
}
|
|
557
|
+
async function parseConfigFile(configPath) {
|
|
558
|
+
const importPath = `${configPath}?t=${Date.now()}`;
|
|
559
|
+
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
560
|
+
return flattenConfig(parsedConfig);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// src/commands/run.ts
|
|
564
|
+
var {$: $2 } = globalThis.Bun;
|
|
565
|
+
var {sleep: sleep2 } = globalThis.Bun;
|
|
566
|
+
import { join as join3 } from "path";
|
|
567
|
+
var homePath2 = getHomeDir();
|
|
568
|
+
async function handleRun(options) {
|
|
569
|
+
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
570
|
+
const existingProcess = name ? getProcess(name) : null;
|
|
571
|
+
if (existingProcess) {
|
|
572
|
+
const finalDirectory2 = directory || existingProcess.workdir;
|
|
573
|
+
validateDirectory(finalDirectory2);
|
|
574
|
+
$2.cwd(finalDirectory2);
|
|
575
|
+
if (fetch) {
|
|
576
|
+
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
577
|
+
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
await $2`git fetch origin`;
|
|
581
|
+
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
582
|
+
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
583
|
+
if (localHash !== remoteHash) {
|
|
584
|
+
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
585
|
+
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
586
|
+
}
|
|
587
|
+
} catch (err) {
|
|
588
|
+
error(`Failed to pull latest changes: ${err}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
592
|
+
if (isRunning && !force) {
|
|
593
|
+
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
594
|
+
}
|
|
595
|
+
let detectedPorts = [];
|
|
596
|
+
if (isRunning) {
|
|
597
|
+
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
598
|
+
}
|
|
599
|
+
if (isRunning) {
|
|
600
|
+
await terminateProcess(existingProcess.pid);
|
|
601
|
+
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
602
|
+
}
|
|
603
|
+
for (const port of detectedPorts) {
|
|
604
|
+
await killProcessOnPort(port);
|
|
605
|
+
}
|
|
606
|
+
for (const port of detectedPorts) {
|
|
607
|
+
const freed = await waitForPortFree(port, 5000);
|
|
608
|
+
if (!freed) {
|
|
609
|
+
await killProcessOnPort(port);
|
|
610
|
+
await waitForPortFree(port, 3000);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
614
|
+
} else {
|
|
615
|
+
if (!directory || !name || !command) {
|
|
616
|
+
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
617
|
+
}
|
|
618
|
+
validateDirectory(directory);
|
|
619
|
+
$2.cwd(directory);
|
|
620
|
+
}
|
|
621
|
+
const finalCommand = command || existingProcess.command;
|
|
622
|
+
const finalDirectory = directory || existingProcess?.workdir;
|
|
623
|
+
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
624
|
+
let finalConfigPath;
|
|
625
|
+
if (configPath !== undefined) {
|
|
626
|
+
finalConfigPath = configPath;
|
|
627
|
+
} else if (existingProcess) {
|
|
628
|
+
finalConfigPath = existingProcess.configPath;
|
|
629
|
+
} else {
|
|
630
|
+
finalConfigPath = ".config.toml";
|
|
631
|
+
}
|
|
632
|
+
if (finalConfigPath) {
|
|
633
|
+
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
634
|
+
if (await Bun.file(fullConfigPath).exists()) {
|
|
635
|
+
try {
|
|
636
|
+
const newConfigEnv = await parseConfigFile(fullConfigPath);
|
|
637
|
+
finalEnv = { ...finalEnv, ...newConfigEnv };
|
|
638
|
+
console.log(`Loaded config from ${finalConfigPath}`);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
647
|
+
Bun.write(stdoutPath, "");
|
|
648
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
649
|
+
Bun.write(stderrPath, "");
|
|
650
|
+
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
651
|
+
env: { ...Bun.env, ...finalEnv },
|
|
652
|
+
cwd: finalDirectory,
|
|
653
|
+
stdout: Bun.file(stdoutPath),
|
|
654
|
+
stderr: Bun.file(stderrPath)
|
|
655
|
+
});
|
|
656
|
+
newProcess.unref();
|
|
657
|
+
await sleep2(100);
|
|
658
|
+
const actualPid = await findChildPid(newProcess.pid);
|
|
659
|
+
await sleep2(400);
|
|
660
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
661
|
+
pid: actualPid,
|
|
662
|
+
workdir: finalDirectory,
|
|
663
|
+
command: finalCommand,
|
|
664
|
+
name,
|
|
665
|
+
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
666
|
+
configPath: finalConfigPath || "",
|
|
667
|
+
stdout_path: stdoutPath,
|
|
668
|
+
stderr_path: stderrPath
|
|
669
|
+
}));
|
|
670
|
+
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/commands/list.ts
|
|
674
|
+
import chalk4 from "chalk";
|
|
675
|
+
|
|
676
|
+
// src/table.ts
|
|
677
|
+
import chalk3 from "chalk";
|
|
678
|
+
function getTerminalWidth() {
|
|
679
|
+
return process.stdout.columns || 120;
|
|
680
|
+
}
|
|
681
|
+
function stripAnsi(str) {
|
|
682
|
+
return str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
683
|
+
}
|
|
684
|
+
function truncateString(str, maxLength) {
|
|
685
|
+
const stripped = stripAnsi(str);
|
|
686
|
+
if (stripped.length <= maxLength)
|
|
687
|
+
return str;
|
|
688
|
+
const ellipsis = "\u2026";
|
|
689
|
+
if (maxLength < 1)
|
|
690
|
+
return "";
|
|
691
|
+
if (maxLength === 1)
|
|
692
|
+
return ellipsis;
|
|
693
|
+
const targetLength = maxLength - ellipsis.length;
|
|
694
|
+
return str.substring(0, targetLength > 0 ? targetLength : 0) + ellipsis;
|
|
695
|
+
}
|
|
696
|
+
function truncatePath(str, maxLength) {
|
|
697
|
+
const stripped = stripAnsi(str);
|
|
698
|
+
if (stripped.length <= maxLength)
|
|
699
|
+
return str;
|
|
700
|
+
const ellipsis = "\u2026";
|
|
701
|
+
if (maxLength < 3)
|
|
702
|
+
return truncateString(str, maxLength);
|
|
703
|
+
const targetLength = maxLength - ellipsis.length;
|
|
704
|
+
const startLen = Math.ceil(targetLength / 2);
|
|
705
|
+
const endLen = Math.floor(targetLength / 2);
|
|
706
|
+
return str.substring(0, startLen) + ellipsis + str.substring(str.length - endLen);
|
|
707
|
+
}
|
|
708
|
+
function calculateColumnWidths(rows, columns, maxWidth, padding = 2) {
|
|
709
|
+
const separatorsWidth = columns.length + 1;
|
|
710
|
+
const paddingWidth = padding * columns.length;
|
|
711
|
+
const availableWidth = maxWidth - separatorsWidth - paddingWidth;
|
|
712
|
+
const naturalWidths = new Map;
|
|
713
|
+
for (const col of columns) {
|
|
714
|
+
let maxNatural = stripAnsi(col.header).length;
|
|
715
|
+
for (const row of rows) {
|
|
716
|
+
const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
|
|
717
|
+
maxNatural = Math.max(maxNatural, stripAnsi(value).length);
|
|
718
|
+
}
|
|
719
|
+
naturalWidths.set(col.key, maxNatural);
|
|
720
|
+
}
|
|
721
|
+
const totalNaturalWidth = Array.from(naturalWidths.values()).reduce((sum, w) => sum + w, 0);
|
|
722
|
+
if (totalNaturalWidth <= availableWidth) {
|
|
723
|
+
return naturalWidths;
|
|
724
|
+
}
|
|
725
|
+
let overage = totalNaturalWidth - availableWidth;
|
|
726
|
+
const currentWidths = new Map(naturalWidths);
|
|
727
|
+
while (overage > 0) {
|
|
728
|
+
let widestColKey = null;
|
|
729
|
+
let maxW = -1;
|
|
730
|
+
for (const [key, width] of currentWidths.entries()) {
|
|
731
|
+
if (width > maxW) {
|
|
732
|
+
maxW = width;
|
|
733
|
+
widestColKey = key;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (widestColKey === null || maxW <= 1) {
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
currentWidths.set(widestColKey, maxW - 1);
|
|
740
|
+
overage--;
|
|
741
|
+
}
|
|
742
|
+
return currentWidths;
|
|
743
|
+
}
|
|
744
|
+
function renderBorder(widths, padding, style) {
|
|
745
|
+
const [left, mid, right, line] = style;
|
|
746
|
+
let lineStr = left;
|
|
747
|
+
for (let i = 0;i < widths.length; i++) {
|
|
748
|
+
lineStr += line.repeat(widths[i] + padding);
|
|
749
|
+
if (i < widths.length - 1) {
|
|
750
|
+
lineStr += mid;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
lineStr += right;
|
|
754
|
+
return lineStr;
|
|
755
|
+
}
|
|
756
|
+
function renderHorizontalTable(rows, columns, options = {}) {
|
|
757
|
+
const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
|
|
758
|
+
if (rows.length === 0)
|
|
759
|
+
return { table: chalk3.gray("No data to display"), truncatedIndices: [] };
|
|
760
|
+
const borderChars = {
|
|
761
|
+
rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
|
|
762
|
+
none: [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]
|
|
763
|
+
}[borderStyle];
|
|
764
|
+
const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars;
|
|
765
|
+
const columnWidths = calculateColumnWidths(rows, columns, maxWidth, padding);
|
|
766
|
+
const widthArray = columns.map((col) => columnWidths.get(col.key));
|
|
767
|
+
const truncatedIndices = new Set;
|
|
768
|
+
const lines = [];
|
|
769
|
+
const cellPadding = " ".repeat(padding / 2);
|
|
770
|
+
if (borderStyle !== "none")
|
|
771
|
+
lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
|
|
772
|
+
if (showHeaders) {
|
|
773
|
+
const headerCells = columns.map((col, i) => chalk3.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
|
|
774
|
+
lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
|
|
775
|
+
if (borderStyle !== "none")
|
|
776
|
+
lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
|
|
777
|
+
}
|
|
778
|
+
rows.forEach((row, rowIndex) => {
|
|
779
|
+
const cells = columns.map((col, i) => {
|
|
780
|
+
const width = widthArray[i];
|
|
781
|
+
const originalValue = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
|
|
782
|
+
if (stripAnsi(originalValue).length > width) {
|
|
783
|
+
truncatedIndices.add(rowIndex);
|
|
784
|
+
}
|
|
785
|
+
const truncator = col.truncator || truncateString;
|
|
786
|
+
const truncated = truncator(originalValue, width);
|
|
787
|
+
return truncated + " ".repeat(Math.max(0, width - stripAnsi(truncated).length));
|
|
788
|
+
});
|
|
789
|
+
lines.push(`${v}${cellPadding}${cells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
|
|
790
|
+
});
|
|
791
|
+
if (borderStyle !== "none")
|
|
792
|
+
lines.push(renderBorder(widthArray, padding, [bl, bc, br, h]));
|
|
793
|
+
return { table: lines.join(`
|
|
794
|
+
`), truncatedIndices: Array.from(truncatedIndices) };
|
|
795
|
+
}
|
|
796
|
+
function renderVerticalTree(rows, columns) {
|
|
797
|
+
const lines = [];
|
|
798
|
+
rows.forEach((row, index) => {
|
|
799
|
+
if (index > 0)
|
|
800
|
+
lines.push("");
|
|
801
|
+
const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
|
|
802
|
+
lines.push(chalk3.cyan(`\u25B6 ${name}`));
|
|
803
|
+
columns.forEach((col) => {
|
|
804
|
+
const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
|
|
805
|
+
lines.push(` \u251C\u2500 ${chalk3.gray(`${col.header}:`)} ${value}`);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
return lines.join(`
|
|
809
|
+
`);
|
|
810
|
+
}
|
|
811
|
+
function renderHybridTable(rows, columns, options = {}) {
|
|
812
|
+
const { table, truncatedIndices } = renderHorizontalTable(rows, columns, options);
|
|
813
|
+
const output = [table];
|
|
814
|
+
if (truncatedIndices.length > 0) {
|
|
815
|
+
const truncatedRows = truncatedIndices.map((i) => rows[i]);
|
|
816
|
+
output.push(`
|
|
817
|
+
` + renderVerticalTree(truncatedRows, columns));
|
|
818
|
+
}
|
|
819
|
+
return output.join(`
|
|
820
|
+
`);
|
|
821
|
+
}
|
|
822
|
+
function renderProcessTable(processes, options) {
|
|
823
|
+
const columns = [
|
|
824
|
+
{ key: "id", header: "ID", formatter: (id) => chalk3.blue(id) },
|
|
825
|
+
{ key: "pid", header: "PID", formatter: (pid) => chalk3.yellow(pid) },
|
|
826
|
+
{ key: "name", header: "Name", formatter: (name) => chalk3.cyan.bold(name) },
|
|
827
|
+
{ key: "port", header: "Port", formatter: (port) => port === "-" ? chalk3.gray(port) : chalk3.hex("#FF6B6B")(port) },
|
|
828
|
+
{ key: "command", header: "Command" },
|
|
829
|
+
{ key: "workdir", header: "Directory", formatter: (dir) => chalk3.gray(dir), truncator: truncatePath },
|
|
830
|
+
{ key: "status", header: "Status" },
|
|
831
|
+
{ key: "runtime", header: "Runtime", formatter: (runtime) => chalk3.magenta(runtime) }
|
|
832
|
+
];
|
|
833
|
+
return renderHybridTable(processes, columns, options);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/commands/list.ts
|
|
837
|
+
init_platform();
|
|
838
|
+
async function showAll(opts) {
|
|
839
|
+
const processes = getAllProcesses();
|
|
840
|
+
const filtered = processes.filter((proc) => {
|
|
841
|
+
if (!opts?.filter)
|
|
842
|
+
return true;
|
|
843
|
+
const envVars = parseEnvString(proc.env);
|
|
844
|
+
return envVars["BGR_GROUP"] === opts.filter;
|
|
845
|
+
});
|
|
846
|
+
if (opts?.json) {
|
|
847
|
+
const jsonData = [];
|
|
848
|
+
for (const proc of filtered) {
|
|
849
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
850
|
+
const envVars = parseEnvString(proc.env);
|
|
851
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
852
|
+
jsonData.push({
|
|
853
|
+
pid: proc.pid,
|
|
854
|
+
name: proc.name,
|
|
855
|
+
ports: ports.length > 0 ? ports : undefined,
|
|
856
|
+
status: isRunning ? "running" : "stopped",
|
|
857
|
+
env: envVars
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
console.log(JSON.stringify(jsonData, null, 2));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const tableData = [];
|
|
864
|
+
for (const proc of filtered) {
|
|
865
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
866
|
+
const runtime = calculateRuntime(proc.timestamp);
|
|
867
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
868
|
+
tableData.push({
|
|
869
|
+
id: proc.id,
|
|
870
|
+
pid: proc.pid,
|
|
871
|
+
name: proc.name,
|
|
872
|
+
port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
|
|
873
|
+
command: proc.command,
|
|
874
|
+
workdir: proc.workdir,
|
|
875
|
+
status: isRunning ? chalk4.green.bold("\u25CF Running") : chalk4.red.bold("\u25CB Stopped"),
|
|
876
|
+
runtime
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (tableData.length === 0) {
|
|
880
|
+
if (opts?.filter) {
|
|
881
|
+
announce(`No processes matched filter BGR_GROUP='${opts.filter}'.`, "No Matches");
|
|
882
|
+
} else {
|
|
883
|
+
announce("No processes found.", "Empty");
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const tableOutput = renderProcessTable(tableData, {
|
|
888
|
+
padding: 1,
|
|
889
|
+
borderStyle: "rounded",
|
|
890
|
+
showHeaders: true
|
|
891
|
+
});
|
|
892
|
+
console.log(tableOutput);
|
|
893
|
+
const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
|
|
894
|
+
const stoppedCount = tableData.filter((p) => p.status.includes("Stopped")).length;
|
|
895
|
+
console.log(chalk4.cyan(`Total: ${tableData.length} processes (${chalk4.green(`${runningCount} running`)}, ${chalk4.red(`${stoppedCount} stopped`)})`));
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/commands/cleanup.ts
|
|
899
|
+
init_platform();
|
|
900
|
+
import * as fs3 from "fs";
|
|
901
|
+
async function handleDelete(name) {
|
|
902
|
+
const process2 = getProcess(name);
|
|
903
|
+
if (!process2) {
|
|
904
|
+
error(`No process found named '${name}'`);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const isRunning = await isProcessRunning(process2.pid);
|
|
908
|
+
if (isRunning) {
|
|
909
|
+
await terminateProcess(process2.pid);
|
|
910
|
+
}
|
|
911
|
+
if (fs3.existsSync(process2.stdout_path)) {
|
|
912
|
+
try {
|
|
913
|
+
fs3.unlinkSync(process2.stdout_path);
|
|
914
|
+
} catch {}
|
|
915
|
+
}
|
|
916
|
+
if (fs3.existsSync(process2.stderr_path)) {
|
|
917
|
+
try {
|
|
918
|
+
fs3.unlinkSync(process2.stderr_path);
|
|
919
|
+
} catch {}
|
|
920
|
+
}
|
|
921
|
+
removeProcessByName(name);
|
|
922
|
+
announce(`Process '${name}' has been ${isRunning ? "stopped and " : ""}deleted`, "Process Deleted");
|
|
923
|
+
}
|
|
924
|
+
async function handleClean() {
|
|
925
|
+
const processes = getAllProcesses();
|
|
926
|
+
let cleanedCount = 0;
|
|
927
|
+
let deletedLogs = 0;
|
|
928
|
+
for (const proc of processes) {
|
|
929
|
+
const running = await isProcessRunning(proc.pid);
|
|
930
|
+
if (!running) {
|
|
931
|
+
removeProcess(proc.pid);
|
|
932
|
+
cleanedCount++;
|
|
933
|
+
if (fs3.existsSync(proc.stdout_path)) {
|
|
934
|
+
try {
|
|
935
|
+
fs3.unlinkSync(proc.stdout_path);
|
|
936
|
+
deletedLogs++;
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
if (fs3.existsSync(proc.stderr_path)) {
|
|
940
|
+
try {
|
|
941
|
+
fs3.unlinkSync(proc.stderr_path);
|
|
942
|
+
deletedLogs++;
|
|
943
|
+
} catch {}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (cleanedCount === 0) {
|
|
948
|
+
announce("No stopped processes found to clean.", "Clean Complete");
|
|
949
|
+
} else {
|
|
950
|
+
announce(`Cleaned ${cleanedCount} stopped ${cleanedCount === 1 ? "process" : "processes"} and removed ${deletedLogs} log ${deletedLogs === 1 ? "file" : "files"}.`, "Clean Complete");
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function handleStop(name) {
|
|
954
|
+
const proc = getProcess(name);
|
|
955
|
+
if (!proc) {
|
|
956
|
+
error(`No process found named '${name}'`);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const isRunning = await isProcessRunning(proc.pid);
|
|
960
|
+
if (!isRunning) {
|
|
961
|
+
announce(`Process '${name}' is already stopped.`, "Process Stop");
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const { getProcessPorts: getProcessPorts2, killProcessOnPort: killProcessOnPort2 } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
965
|
+
const ports = await getProcessPorts2(proc.pid);
|
|
966
|
+
await terminateProcess(proc.pid);
|
|
967
|
+
for (const port of ports) {
|
|
968
|
+
await killProcessOnPort2(port);
|
|
969
|
+
}
|
|
970
|
+
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
971
|
+
}
|
|
972
|
+
async function handleDeleteAll() {
|
|
973
|
+
const processes = getAllProcesses();
|
|
974
|
+
if (processes.length === 0) {
|
|
975
|
+
announce("There are no processes to delete.", "Delete All");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
const { getProcessPorts: getProcessPorts2, killProcessOnPort: killProcessOnPort2, waitForPortFree: waitForPortFree2 } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
979
|
+
let killedCount = 0;
|
|
980
|
+
let portsFreed = 0;
|
|
981
|
+
for (const proc of processes) {
|
|
982
|
+
const running = await isProcessRunning(proc.pid);
|
|
983
|
+
if (running) {
|
|
984
|
+
const ports = await getProcessPorts2(proc.pid);
|
|
985
|
+
await terminateProcess(proc.pid, true);
|
|
986
|
+
killedCount++;
|
|
987
|
+
for (const port of ports) {
|
|
988
|
+
await killProcessOnPort2(port);
|
|
989
|
+
const freed = await waitForPortFree2(port, 3000);
|
|
990
|
+
if (!freed) {
|
|
991
|
+
await killProcessOnPort2(port);
|
|
992
|
+
await waitForPortFree2(port, 2000);
|
|
993
|
+
}
|
|
994
|
+
portsFreed++;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (fs3.existsSync(proc.stdout_path)) {
|
|
998
|
+
try {
|
|
999
|
+
fs3.unlinkSync(proc.stdout_path);
|
|
1000
|
+
} catch {}
|
|
1001
|
+
}
|
|
1002
|
+
if (fs3.existsSync(proc.stderr_path)) {
|
|
1003
|
+
try {
|
|
1004
|
+
fs3.unlinkSync(proc.stderr_path);
|
|
1005
|
+
} catch {}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
removeAllProcesses();
|
|
1009
|
+
const parts = [`${processes.length} ${processes.length === 1 ? "process" : "processes"} deleted`];
|
|
1010
|
+
if (killedCount > 0)
|
|
1011
|
+
parts.push(`${killedCount} force-killed`);
|
|
1012
|
+
if (portsFreed > 0)
|
|
1013
|
+
parts.push(`${portsFreed} ${portsFreed === 1 ? "port" : "ports"} freed`);
|
|
1014
|
+
announce(parts.join(", ") + ".", "Nuke Complete");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/commands/watch.ts
|
|
1018
|
+
init_platform();
|
|
1019
|
+
import * as fs4 from "fs";
|
|
1020
|
+
import { join as join4 } from "path";
|
|
1021
|
+
import path from "path";
|
|
1022
|
+
import chalk5 from "chalk";
|
|
1023
|
+
async function handleWatch(options, logOptions) {
|
|
1024
|
+
let currentProcess = null;
|
|
1025
|
+
let isRestarting = false;
|
|
1026
|
+
let debounceTimeout = null;
|
|
1027
|
+
let tailStops = [];
|
|
1028
|
+
let lastRestartPath = null;
|
|
1029
|
+
const dumpLogsIfDead = async (proc, reason) => {
|
|
1030
|
+
const isDead = !await isProcessRunning(proc.pid);
|
|
1031
|
+
if (!isDead)
|
|
1032
|
+
return false;
|
|
1033
|
+
console.log(chalk5.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
|
|
1034
|
+
const readAndDump = (path2, color, label) => {
|
|
1035
|
+
try {
|
|
1036
|
+
if (fs4.existsSync(path2)) {
|
|
1037
|
+
const content = fs4.readFileSync(path2, "utf8").trim();
|
|
1038
|
+
if (content) {
|
|
1039
|
+
console.log(`${color.bold(label)}:
|
|
1040
|
+
${color(content)}
|
|
1041
|
+
`);
|
|
1042
|
+
} else {
|
|
1043
|
+
console.log(`${color(label)}: (empty)`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
} catch (err) {
|
|
1047
|
+
console.warn(chalk5.gray(`Could not read ${label} log: ${err}`));
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1051
|
+
readAndDump(proc.stdout_path, chalk5.white, "\uD83D\uDCC4 Stdout");
|
|
1052
|
+
}
|
|
1053
|
+
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1054
|
+
readAndDump(proc.stderr_path, chalk5.red, "\uD83D\uDCC4 Stderr");
|
|
1055
|
+
}
|
|
1056
|
+
return true;
|
|
1057
|
+
};
|
|
1058
|
+
const waitForLogReady = (logPath, timeoutMs = 5000) => {
|
|
1059
|
+
return new Promise((resolve, reject) => {
|
|
1060
|
+
const checkReady = () => {
|
|
1061
|
+
try {
|
|
1062
|
+
if (fs4.existsSync(logPath)) {
|
|
1063
|
+
const stat = fs4.statSync(logPath);
|
|
1064
|
+
if (stat.size > 0) {
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
} catch {}
|
|
1069
|
+
return false;
|
|
1070
|
+
};
|
|
1071
|
+
if (checkReady()) {
|
|
1072
|
+
resolve();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const dir = path.dirname(logPath);
|
|
1076
|
+
const filename = path.basename(logPath);
|
|
1077
|
+
const watcher2 = fs4.watch(dir, (eventType, changedFilename) => {
|
|
1078
|
+
if (changedFilename === filename && eventType === "change") {
|
|
1079
|
+
if (checkReady()) {
|
|
1080
|
+
watcher2.close();
|
|
1081
|
+
resolve();
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
setTimeout(() => {
|
|
1086
|
+
watcher2.close();
|
|
1087
|
+
reject(new Error(`Log file ${logPath} did not become ready within ${timeoutMs}ms`));
|
|
1088
|
+
}, timeoutMs);
|
|
1089
|
+
});
|
|
1090
|
+
};
|
|
1091
|
+
const startTails = async () => {
|
|
1092
|
+
const stops = [];
|
|
1093
|
+
if (!logOptions.showLogs || !currentProcess)
|
|
1094
|
+
return stops;
|
|
1095
|
+
console.log(chalk5.gray(`
|
|
1096
|
+
` + "\u2500".repeat(50) + `
|
|
1097
|
+
`));
|
|
1098
|
+
if (logOptions.logType === "both" || logOptions.logType === "stdout") {
|
|
1099
|
+
console.log(chalk5.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
|
|
1100
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1101
|
+
try {
|
|
1102
|
+
await waitForLogReady(currentProcess.stdout_path);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
console.warn(chalk5.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1105
|
+
}
|
|
1106
|
+
const stop = tailFile(currentProcess.stdout_path, "", chalk5.white, logOptions.lines);
|
|
1107
|
+
stops.push(stop);
|
|
1108
|
+
}
|
|
1109
|
+
if (logOptions.logType === "both" || logOptions.logType === "stderr") {
|
|
1110
|
+
console.log(chalk5.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
|
|
1111
|
+
console.log(chalk5.gray("\u2550".repeat(50)));
|
|
1112
|
+
try {
|
|
1113
|
+
await waitForLogReady(currentProcess.stderr_path);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
console.warn(chalk5.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
|
|
1116
|
+
}
|
|
1117
|
+
const stop = tailFile(currentProcess.stderr_path, "", chalk5.red, logOptions.lines);
|
|
1118
|
+
stops.push(stop);
|
|
1119
|
+
}
|
|
1120
|
+
return stops;
|
|
1121
|
+
};
|
|
1122
|
+
const restartProcess = async (path2) => {
|
|
1123
|
+
if (isRestarting)
|
|
1124
|
+
return;
|
|
1125
|
+
isRestarting = true;
|
|
1126
|
+
const restartReason = path2 ? `restart (change in ${path2})` : "initial start";
|
|
1127
|
+
lastRestartPath = path2 || null;
|
|
1128
|
+
tailStops.forEach((stop) => stop());
|
|
1129
|
+
tailStops = [];
|
|
1130
|
+
console.clear();
|
|
1131
|
+
announce(`\uD83D\uDD04 Restarting process '${options.name}'... [${restartReason}]`, "Watch Mode");
|
|
1132
|
+
try {
|
|
1133
|
+
await handleRun({ ...options, force: true });
|
|
1134
|
+
currentProcess = getProcess(options.name);
|
|
1135
|
+
if (!currentProcess) {
|
|
1136
|
+
error(`Failed to find process '${options.name}' after restart.`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const died = await dumpLogsIfDead(currentProcess, restartReason);
|
|
1140
|
+
if (died) {
|
|
1141
|
+
if (lastRestartPath) {
|
|
1142
|
+
console.log(chalk5.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
|
|
1143
|
+
return;
|
|
1144
|
+
} else {
|
|
1145
|
+
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
tailStops = await startTails();
|
|
1150
|
+
} catch (err) {
|
|
1151
|
+
error(`Error during restart: ${err}`);
|
|
1152
|
+
} finally {
|
|
1153
|
+
isRestarting = false;
|
|
1154
|
+
if (currentProcess) {
|
|
1155
|
+
console.log(chalk5.cyan(`
|
|
1156
|
+
\uD83D\uDC40 Watching for file changes in: ${currentProcess.workdir}`));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
console.clear();
|
|
1161
|
+
announce(`\uD83D\uDE80 Starting initial process '${options.name}' in watch mode...`, "Watch Mode");
|
|
1162
|
+
await handleRun(options);
|
|
1163
|
+
currentProcess = getProcess(options.name);
|
|
1164
|
+
if (!currentProcess) {
|
|
1165
|
+
error(`Could not start or find process '${options.name}'. Aborting watch mode.`);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const initialDied = await dumpLogsIfDead(currentProcess, "initial start");
|
|
1169
|
+
if (initialDied) {
|
|
1170
|
+
error(`Failed to start process '${options.name}'. Aborting watch mode.`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
tailStops = await startTails();
|
|
1174
|
+
const workdir = currentProcess.workdir;
|
|
1175
|
+
console.log(chalk5.cyan(`
|
|
1176
|
+
\uD83D\uDC40 Watching for file changes in: ${workdir}`));
|
|
1177
|
+
const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
1178
|
+
if (filename == null)
|
|
1179
|
+
return;
|
|
1180
|
+
const fullPath = join4(workdir, filename);
|
|
1181
|
+
if (fullPath.includes(".git") || fullPath.includes("node_modules"))
|
|
1182
|
+
return;
|
|
1183
|
+
if (debounceTimeout)
|
|
1184
|
+
clearTimeout(debounceTimeout);
|
|
1185
|
+
debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
|
|
1186
|
+
});
|
|
1187
|
+
const cleanup = async () => {
|
|
1188
|
+
console.log(chalk5.magenta(`
|
|
1189
|
+
SIGINT received...`));
|
|
1190
|
+
watcher.close();
|
|
1191
|
+
tailStops.forEach((stop) => stop());
|
|
1192
|
+
if (debounceTimeout)
|
|
1193
|
+
clearTimeout(debounceTimeout);
|
|
1194
|
+
const procToKill = getProcess(options.name);
|
|
1195
|
+
if (procToKill) {
|
|
1196
|
+
const isRunning = await isProcessRunning(procToKill.pid);
|
|
1197
|
+
if (isRunning) {
|
|
1198
|
+
console.log(`process ${procToKill.name} (PID: ${procToKill.pid}) still running`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
process.exit(0);
|
|
1202
|
+
};
|
|
1203
|
+
process.on("SIGINT", cleanup);
|
|
1204
|
+
process.on("SIGTERM", cleanup);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// src/commands/logs.ts
|
|
1208
|
+
init_platform();
|
|
1209
|
+
import chalk6 from "chalk";
|
|
1210
|
+
import * as fs5 from "fs";
|
|
1211
|
+
async function showLogs(name, logType = "both", lines) {
|
|
1212
|
+
const proc = getProcess(name);
|
|
1213
|
+
if (!proc) {
|
|
1214
|
+
error(`No process found named '${name}'`);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (logType === "both" || logType === "stdout") {
|
|
1218
|
+
console.log(chalk6.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
|
|
1219
|
+
console.log(chalk6.gray("\u2550".repeat(50)));
|
|
1220
|
+
if (fs5.existsSync(proc.stdout_path)) {
|
|
1221
|
+
try {
|
|
1222
|
+
const output = await readFileTail(proc.stdout_path, lines);
|
|
1223
|
+
console.log(output || chalk6.gray("(no output)"));
|
|
1224
|
+
} catch (err) {
|
|
1225
|
+
console.log(chalk6.red(`Error reading stdout: ${err}`));
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
console.log(chalk6.gray("(log file not found)"));
|
|
1229
|
+
}
|
|
1230
|
+
if (logType === "both") {
|
|
1231
|
+
console.log(`
|
|
1232
|
+
`);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
if (logType === "both" || logType === "stderr") {
|
|
1236
|
+
console.log(chalk6.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
|
|
1237
|
+
console.log(chalk6.gray("\u2550".repeat(50)));
|
|
1238
|
+
if (fs5.existsSync(proc.stderr_path)) {
|
|
1239
|
+
try {
|
|
1240
|
+
const output = await readFileTail(proc.stderr_path, lines);
|
|
1241
|
+
console.log(output || chalk6.gray("(no errors)"));
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
console.log(chalk6.red(`Error reading stderr: ${err}`));
|
|
1244
|
+
}
|
|
1245
|
+
} else {
|
|
1246
|
+
console.log(chalk6.gray("(log file not found)"));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/commands/details.ts
|
|
1252
|
+
init_platform();
|
|
1253
|
+
import chalk7 from "chalk";
|
|
1254
|
+
async function showDetails(name) {
|
|
1255
|
+
const proc = getProcess(name);
|
|
1256
|
+
if (!proc) {
|
|
1257
|
+
error(`No process found named '${name}'`);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
1261
|
+
const runtime = calculateRuntime(proc.timestamp);
|
|
1262
|
+
const envVars = parseEnvString(proc.env);
|
|
1263
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
1264
|
+
const portDisplay = ports.length > 0 ? ports.map((p) => chalk7.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
|
|
1265
|
+
const details = `
|
|
1266
|
+
${chalk7.bold("Process Details:")}
|
|
1267
|
+
${chalk7.gray("\u2550".repeat(50))}
|
|
1268
|
+
${chalk7.cyan.bold("Name:")} ${proc.name}
|
|
1269
|
+
${chalk7.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
|
|
1270
|
+
${chalk7.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
|
|
1271
|
+
${chalk7.bold("Status:")} ${isRunning ? chalk7.green.bold("\u25CF Running") : chalk7.red.bold("\u25CB Stopped")}
|
|
1272
|
+
${chalk7.magenta.bold("Runtime:")} ${runtime}
|
|
1273
|
+
${chalk7.blue.bold("Working Directory:")} ${proc.workdir}
|
|
1274
|
+
${chalk7.white.bold("Command:")} ${proc.command}
|
|
1275
|
+
${chalk7.gray.bold("Config Path:")} ${proc.configPath}
|
|
1276
|
+
${chalk7.green.bold("Stdout Path:")} ${proc.stdout_path}
|
|
1277
|
+
${chalk7.red.bold("Stderr Path:")} ${proc.stderr_path}
|
|
1278
|
+
|
|
1279
|
+
${chalk7.bold("\uD83D\uDD27 Environment Variables:")}
|
|
1280
|
+
${chalk7.gray("\u2550".repeat(50))}
|
|
1281
|
+
${Object.entries(envVars).map(([key, value]) => `${chalk7.cyan.bold(key)} = ${chalk7.yellow(value)}`).join(`
|
|
1282
|
+
`)}
|
|
1283
|
+
`;
|
|
1284
|
+
announce(details, `Process Details: ${name}`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/server.ts
|
|
1288
|
+
import { start } from "melina";
|
|
1289
|
+
import path2 from "path";
|
|
1290
|
+
async function startServer() {
|
|
1291
|
+
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
1292
|
+
const port = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
|
|
1293
|
+
await start({
|
|
1294
|
+
appDir,
|
|
1295
|
+
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1296
|
+
globalCss: path2.join(appDir, "globals.css"),
|
|
1297
|
+
port
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/index.ts
|
|
1302
|
+
init_platform();
|
|
1303
|
+
import dedent from "dedent";
|
|
1304
|
+
import chalk8 from "chalk";
|
|
1305
|
+
import { join as join5 } from "path";
|
|
1306
|
+
var {sleep: sleep3 } = globalThis.Bun;
|
|
1307
|
+
async function showHelp() {
|
|
1308
|
+
const usage = dedent`
|
|
1309
|
+
${chalk8.bold("bgrun \u2014 Bun Background Runner")}
|
|
1310
|
+
${chalk8.gray("\u2550".repeat(50))}
|
|
1311
|
+
|
|
1312
|
+
${chalk8.yellow("Usage:")}
|
|
1313
|
+
bgrun [name] [options]
|
|
1314
|
+
|
|
1315
|
+
${chalk8.yellow("Commands:")}
|
|
1316
|
+
bgrun List all processes
|
|
1317
|
+
bgrun [name] Show details for a process
|
|
1318
|
+
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
1319
|
+
bgrun --restart [name] Restart a process
|
|
1320
|
+
bgrun --stop [name] Stop a process (keep in registry)
|
|
1321
|
+
bgrun --delete [name] Delete a process
|
|
1322
|
+
bgrun --clean Remove all stopped processes
|
|
1323
|
+
bgrun --nuke Delete ALL processes
|
|
1324
|
+
|
|
1325
|
+
${chalk8.yellow("Options:")}
|
|
1326
|
+
--name <string> Process name (required for new)
|
|
1327
|
+
--command <string> Process command (required for new)
|
|
1328
|
+
--directory <path> Working directory (required for new)
|
|
1329
|
+
--config <path> Config file (default: .config.toml)
|
|
1330
|
+
--watch Watch for file changes and auto-restart
|
|
1331
|
+
--force Force restart existing process
|
|
1332
|
+
--fetch Fetch latest git changes before running
|
|
1333
|
+
--json Output in JSON format
|
|
1334
|
+
--filter <group> Filter list by BGR_GROUP
|
|
1335
|
+
--logs Show logs
|
|
1336
|
+
--log-stdout Show only stdout logs
|
|
1337
|
+
--log-stderr Show only stderr logs
|
|
1338
|
+
--lines <n> Number of log lines to show (default: all)
|
|
1339
|
+
--version Show version
|
|
1340
|
+
--dashboard Launch web dashboard as bgrun-managed process
|
|
1341
|
+
--port <number> Port for dashboard (default: 3000)
|
|
1342
|
+
--help Show this help message
|
|
1343
|
+
|
|
1344
|
+
${chalk8.yellow("Examples:")}
|
|
1345
|
+
bgrun --dashboard
|
|
1346
|
+
bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
1347
|
+
bgrun myapp --logs --lines 50
|
|
1348
|
+
`;
|
|
1349
|
+
console.log(usage);
|
|
1350
|
+
}
|
|
1351
|
+
async function run() {
|
|
1352
|
+
const { values, positionals } = parseArgs({
|
|
1353
|
+
args: Bun.argv.slice(2),
|
|
1354
|
+
options: {
|
|
1355
|
+
name: { type: "string" },
|
|
1356
|
+
command: { type: "string" },
|
|
1357
|
+
directory: { type: "string" },
|
|
1358
|
+
config: { type: "string" },
|
|
1359
|
+
watch: { type: "boolean" },
|
|
1360
|
+
force: { type: "boolean" },
|
|
1361
|
+
fetch: { type: "boolean" },
|
|
1362
|
+
delete: { type: "boolean" },
|
|
1363
|
+
nuke: { type: "boolean" },
|
|
1364
|
+
restart: { type: "boolean" },
|
|
1365
|
+
stop: { type: "boolean" },
|
|
1366
|
+
clean: { type: "boolean" },
|
|
1367
|
+
json: { type: "boolean" },
|
|
1368
|
+
logs: { type: "boolean" },
|
|
1369
|
+
"log-stdout": { type: "boolean" },
|
|
1370
|
+
"log-stderr": { type: "boolean" },
|
|
1371
|
+
lines: { type: "string" },
|
|
1372
|
+
filter: { type: "string" },
|
|
1373
|
+
version: { type: "boolean" },
|
|
1374
|
+
help: { type: "boolean" },
|
|
1375
|
+
db: { type: "string" },
|
|
1376
|
+
stdout: { type: "string" },
|
|
1377
|
+
stderr: { type: "string" },
|
|
1378
|
+
dashboard: { type: "boolean" },
|
|
1379
|
+
_serve: { type: "boolean" },
|
|
1380
|
+
port: { type: "string" }
|
|
1381
|
+
},
|
|
1382
|
+
strict: false,
|
|
1383
|
+
allowPositionals: true
|
|
1384
|
+
});
|
|
1385
|
+
if (values["_serve"]) {
|
|
1386
|
+
await startServer();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (values.dashboard) {
|
|
1390
|
+
const dashboardName = "bgr-dashboard";
|
|
1391
|
+
const homePath3 = getHomeDir();
|
|
1392
|
+
const bgrDir = join5(homePath3, ".bgr");
|
|
1393
|
+
const requestedPort = values.port;
|
|
1394
|
+
const existing = getProcess(dashboardName);
|
|
1395
|
+
if (existing && await isProcessRunning(existing.pid)) {
|
|
1396
|
+
const existingPorts = await getProcessPorts(existing.pid);
|
|
1397
|
+
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
|
|
1398
|
+
announce(`Dashboard is already running (PID ${existing.pid})
|
|
1399
|
+
|
|
1400
|
+
` + ` \uD83C\uDF10 ${chalk8.cyan(`http://localhost${portStr}`)}
|
|
1401
|
+
|
|
1402
|
+
` + ` Use ${chalk8.yellow(`bgrun --stop ${dashboardName}`)} to stop it
|
|
1403
|
+
` + ` Use ${chalk8.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (existing) {
|
|
1407
|
+
if (await isProcessRunning(existing.pid)) {
|
|
1408
|
+
const detectedPorts = await getProcessPorts(existing.pid);
|
|
1409
|
+
await terminateProcess(existing.pid);
|
|
1410
|
+
for (const p of detectedPorts) {
|
|
1411
|
+
await killProcessOnPort(p);
|
|
1412
|
+
await waitForPortFree(p, 5000);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
await retryDatabaseOperation(() => removeProcessByName(dashboardName));
|
|
1416
|
+
}
|
|
1417
|
+
const { resolve } = __require("path");
|
|
1418
|
+
const scriptPath = resolve(process.argv[1]);
|
|
1419
|
+
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
1420
|
+
const command = `bgrun --_serve`;
|
|
1421
|
+
const stdoutPath = join5(bgrDir, `${dashboardName}-out.txt`);
|
|
1422
|
+
const stderrPath = join5(bgrDir, `${dashboardName}-err.txt`);
|
|
1423
|
+
await Bun.write(stdoutPath, "");
|
|
1424
|
+
await Bun.write(stderrPath, "");
|
|
1425
|
+
const spawnEnv = { ...Bun.env };
|
|
1426
|
+
if (requestedPort) {
|
|
1427
|
+
spawnEnv.BUN_PORT = requestedPort;
|
|
1428
|
+
}
|
|
1429
|
+
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
1430
|
+
env: spawnEnv,
|
|
1431
|
+
cwd: bgrDir,
|
|
1432
|
+
stdout: Bun.file(stdoutPath),
|
|
1433
|
+
stderr: Bun.file(stderrPath)
|
|
1434
|
+
});
|
|
1435
|
+
newProcess.unref();
|
|
1436
|
+
await sleep3(2000);
|
|
1437
|
+
const actualPid = await findChildPid(newProcess.pid);
|
|
1438
|
+
let actualPort = null;
|
|
1439
|
+
for (let attempt = 0;attempt < 10; attempt++) {
|
|
1440
|
+
const ports = await getProcessPorts(actualPid);
|
|
1441
|
+
if (ports.length > 0) {
|
|
1442
|
+
actualPort = ports[0];
|
|
1443
|
+
break;
|
|
1444
|
+
}
|
|
1445
|
+
await sleep3(1000);
|
|
1446
|
+
}
|
|
1447
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
1448
|
+
pid: actualPid,
|
|
1449
|
+
workdir: bgrDir,
|
|
1450
|
+
command,
|
|
1451
|
+
name: dashboardName,
|
|
1452
|
+
env: "",
|
|
1453
|
+
configPath: "",
|
|
1454
|
+
stdout_path: stdoutPath,
|
|
1455
|
+
stderr_path: stderrPath
|
|
1456
|
+
}));
|
|
1457
|
+
const portDisplay = actualPort ? String(actualPort) : "(detecting...)";
|
|
1458
|
+
const urlDisplay = actualPort ? `http://localhost:${actualPort}` : "http://localhost (port auto-assigned)";
|
|
1459
|
+
const msg = dedent`
|
|
1460
|
+
${chalk8.bold("\u26A1 BGR Dashboard launched")}
|
|
1461
|
+
${chalk8.gray("\u2500".repeat(40))}
|
|
1462
|
+
|
|
1463
|
+
\u{1f310} Open in browser: ${chalk8.cyan.underline(urlDisplay)}
|
|
1464
|
+
\u{1f4ca} Manage all your processes from the web UI
|
|
1465
|
+
\u{1f504} Auto-refreshes every 3 seconds
|
|
1466
|
+
|
|
1467
|
+
${chalk8.gray("\u2500".repeat(40))}
|
|
1468
|
+
Process: ${chalk8.white(dashboardName)} | PID: ${chalk8.white(String(actualPid))} | Port: ${chalk8.white(portDisplay)}
|
|
1469
|
+
|
|
1470
|
+
${chalk8.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
|
|
1471
|
+
${chalk8.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
|
|
1472
|
+
${chalk8.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
|
|
1473
|
+
`;
|
|
1474
|
+
announce(msg, "BGR Dashboard");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (values.version) {
|
|
1478
|
+
console.log(`bgrun version: ${await getVersion()}`);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (values.help) {
|
|
1482
|
+
await showHelp();
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (values.nuke) {
|
|
1486
|
+
await handleDeleteAll();
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (values.clean) {
|
|
1490
|
+
await handleClean();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
const name = values.name || positionals[0];
|
|
1494
|
+
if (values.delete) {
|
|
1495
|
+
if (name) {
|
|
1496
|
+
await handleDelete(name);
|
|
1497
|
+
} else {
|
|
1498
|
+
error("Please specify a process name to delete.");
|
|
1499
|
+
}
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (values.restart) {
|
|
1503
|
+
if (!name) {
|
|
1504
|
+
error("Please specify a process name to restart.");
|
|
1505
|
+
}
|
|
1506
|
+
await handleRun({
|
|
1507
|
+
action: "run",
|
|
1508
|
+
name,
|
|
1509
|
+
force: true,
|
|
1510
|
+
remoteName: ""
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (values.stop) {
|
|
1515
|
+
if (!name) {
|
|
1516
|
+
error("Please specify a process name to stop.");
|
|
1517
|
+
}
|
|
1518
|
+
await handleStop(name);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (values.logs || values["log-stdout"] || values["log-stderr"]) {
|
|
1522
|
+
if (!name) {
|
|
1523
|
+
error("Please specify a process name to show logs for.");
|
|
1524
|
+
}
|
|
1525
|
+
const logType = values["log-stdout"] ? "stdout" : values["log-stderr"] ? "stderr" : "both";
|
|
1526
|
+
const lines = values.lines ? parseInt(values.lines) : undefined;
|
|
1527
|
+
await showLogs(name, logType, lines);
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (values.watch) {
|
|
1531
|
+
await handleWatch({
|
|
1532
|
+
action: "watch",
|
|
1533
|
+
name,
|
|
1534
|
+
command: values.command,
|
|
1535
|
+
directory: values.directory,
|
|
1536
|
+
configPath: values.config,
|
|
1537
|
+
force: values.force,
|
|
1538
|
+
remoteName: "",
|
|
1539
|
+
dbPath: values.db,
|
|
1540
|
+
stdout: values.stdout,
|
|
1541
|
+
stderr: values.stderr
|
|
1542
|
+
}, {
|
|
1543
|
+
showLogs: values.logs || false,
|
|
1544
|
+
logType: "both",
|
|
1545
|
+
lines: values.lines ? parseInt(values.lines) : undefined
|
|
1546
|
+
});
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (name) {
|
|
1550
|
+
if (!values.command && !values.directory) {
|
|
1551
|
+
await showDetails(name);
|
|
1552
|
+
} else {
|
|
1553
|
+
await handleRun({
|
|
1554
|
+
action: "run",
|
|
1555
|
+
name,
|
|
1556
|
+
command: values.command,
|
|
1557
|
+
directory: values.directory,
|
|
1558
|
+
configPath: values.config,
|
|
1559
|
+
force: values.force,
|
|
1560
|
+
fetch: values.fetch,
|
|
1561
|
+
remoteName: "",
|
|
1562
|
+
dbPath: values.db,
|
|
1563
|
+
stdout: values.stdout,
|
|
1564
|
+
stderr: values.stderr
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
} else {
|
|
1568
|
+
if (values.command) {
|
|
1569
|
+
error("Process name is required.");
|
|
1570
|
+
}
|
|
1571
|
+
await showAll({
|
|
1572
|
+
json: values.json,
|
|
1573
|
+
filter: values.filter
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
run().catch((err) => {
|
|
1578
|
+
console.error(chalk8.red(err));
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
});
|