bgrun 3.12.10 → 3.12.12

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/dist/server.js ADDED
@@ -0,0 +1,1488 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = import.meta.require;
18
+
19
+ // src/platform.ts
20
+ var exports_platform = {};
21
+ __export(exports_platform, {
22
+ waitForPortFree: () => waitForPortFree,
23
+ terminateProcess: () => terminateProcess,
24
+ reconcileProcessPids: () => reconcileProcessPids,
25
+ readFileTail: () => readFileTail,
26
+ psExec: () => psExec,
27
+ parseUnixListeningPorts: () => parseUnixListeningPorts,
28
+ killProcessOnPort: () => killProcessOnPort,
29
+ isWindows: () => isWindows,
30
+ isProcessRunning: () => isProcessRunning,
31
+ isPortFree: () => isPortFree,
32
+ getShellCommand: () => getShellCommand,
33
+ getProcessPorts: () => getProcessPorts,
34
+ getProcessMemory: () => getProcessMemory,
35
+ getProcessBatchResources: () => getProcessBatchResources,
36
+ getPortInfo: () => getPortInfo,
37
+ getHomeDir: () => getHomeDir,
38
+ findPidByPort: () => findPidByPort,
39
+ findChildPid: () => findChildPid,
40
+ ensureDir: () => ensureDir,
41
+ copyFile: () => copyFile
42
+ });
43
+ import * as fs from "fs";
44
+ import * as os from "os";
45
+ import { join } from "path";
46
+ var {$ } = globalThis.Bun;
47
+ import { createMeasure } from "measure-fn";
48
+ function psExec(command, _timeoutMs = 3000) {
49
+ const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
50
+ try {
51
+ fs.writeFileSync(tmpFile, command);
52
+ const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
53
+ try {
54
+ fs.unlinkSync(tmpFile);
55
+ } catch {}
56
+ return result.stdout?.toString() || "";
57
+ } catch {
58
+ try {
59
+ fs.unlinkSync(tmpFile);
60
+ } catch {}
61
+ return "";
62
+ }
63
+ }
64
+ function isWindows() {
65
+ return process.platform === "win32";
66
+ }
67
+ function getHomeDir() {
68
+ return os.homedir();
69
+ }
70
+ async function isProcessRunning(pid, command) {
71
+ if (pid <= 0)
72
+ return false;
73
+ return await plat.measure(`PID ${pid} alive?`, async () => {
74
+ try {
75
+ if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
76
+ return await isDockerContainerRunning(command);
77
+ }
78
+ if (isWindows()) {
79
+ try {
80
+ process.kill(pid, 0);
81
+ return true;
82
+ } catch {
83
+ const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
84
+ return output === String(pid);
85
+ }
86
+ } else {
87
+ const result = await $`ps -p ${pid}`.nothrow().text();
88
+ return result.includes(`${pid}`);
89
+ }
90
+ } catch {
91
+ return false;
92
+ }
93
+ }) ?? false;
94
+ }
95
+ async function isDockerContainerRunning(command) {
96
+ try {
97
+ const nameMatch = command.match(/--name\s+["']?(\S+?)["']?(?:\s|$)/);
98
+ if (nameMatch) {
99
+ const containerName = nameMatch[1];
100
+ const result = await $`docker inspect -f "{{.State.Running}}" ${containerName}`.nothrow().text();
101
+ return result.trim() === "true";
102
+ }
103
+ const imageMatch = command.match(/docker\s+run\s+.*?(?:-d\s+)?(\S+)\s*$/);
104
+ if (imageMatch) {
105
+ const imageName = imageMatch[1];
106
+ const result = await $`docker ps --filter ancestor=${imageName} --format "{{.ID}}"`.nothrow().text();
107
+ return result.trim().length > 0;
108
+ }
109
+ return false;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ async function getChildPids(pid) {
115
+ try {
116
+ if (isWindows()) {
117
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
118
+ return result.split(`
119
+ `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
120
+ } else {
121
+ const result = await $`ps --no-headers -o pid --ppid ${pid}`.nothrow().text();
122
+ return result.trim().split(`
123
+ `).filter((p) => p.trim()).map((p) => parseInt(p)).filter((n) => !isNaN(n));
124
+ }
125
+ } catch {
126
+ return [];
127
+ }
128
+ }
129
+ async function terminateProcess(pid, force = false) {
130
+ await plat.measure(`Terminate PID ${pid}`, async (m) => {
131
+ try {
132
+ if (isWindows()) {
133
+ await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
134
+ } else {
135
+ const children = await m("Get children", () => getChildPids(pid)) ?? [];
136
+ const signal = force ? "KILL" : "TERM";
137
+ for (const childPid of children) {
138
+ try {
139
+ await $`kill -${signal} ${childPid}`.nothrow();
140
+ } catch {}
141
+ }
142
+ await Bun.sleep(500);
143
+ if (await isProcessRunning(pid)) {
144
+ await $`kill -${signal} ${pid}`.nothrow();
145
+ }
146
+ }
147
+ } catch {}
148
+ await Bun.sleep(300);
149
+ });
150
+ }
151
+ async function isPortFree(port) {
152
+ try {
153
+ if (isWindows()) {
154
+ const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
155
+ for (const line of result.split(`
156
+ `)) {
157
+ const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
158
+ if (match) {
159
+ const pid = parseInt(match[2]);
160
+ if (pid > 0 && await isProcessRunning(pid)) {
161
+ return false;
162
+ }
163
+ }
164
+ }
165
+ return true;
166
+ } else {
167
+ const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
168
+ const lines = result.trim().split(`
169
+ `).filter((l) => l.trim());
170
+ return lines.length <= 1;
171
+ }
172
+ } catch {
173
+ return true;
174
+ }
175
+ }
176
+ async function getPortInfo(port) {
177
+ try {
178
+ if (isWindows()) {
179
+ const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
180
+ for (const line of result.split(`
181
+ `)) {
182
+ const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
183
+ if (match) {
184
+ const pid = parseInt(match[2]);
185
+ if (pid > 0 && await isProcessRunning(pid)) {
186
+ const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
187
+ return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
188
+ }
189
+ }
190
+ }
191
+ return { inUse: false };
192
+ } else {
193
+ const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
194
+ const lines = result.trim().split(`
195
+ `).filter((l) => l.trim());
196
+ if (lines.length > 1) {
197
+ return { inUse: true };
198
+ }
199
+ return { inUse: false };
200
+ }
201
+ } catch {
202
+ return { inUse: false };
203
+ }
204
+ }
205
+ async function waitForPortFree(port, timeoutMs = 5000) {
206
+ const startTime = Date.now();
207
+ const pollInterval = 300;
208
+ while (Date.now() - startTime < timeoutMs) {
209
+ if (await isPortFree(port)) {
210
+ return true;
211
+ }
212
+ await Bun.sleep(pollInterval);
213
+ }
214
+ return false;
215
+ }
216
+ async function killProcessOnPort(port) {
217
+ try {
218
+ if (isWindows()) {
219
+ const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
220
+ const pids = new Set;
221
+ for (const line of result.split(`
222
+ `)) {
223
+ const match = line.match(new RegExp(`:(${port})\\s+.*?\\s+(\\d+)\\s*$`));
224
+ if (match && parseInt(match[1]) === port) {
225
+ const pid = parseInt(match[2]);
226
+ if (pid > 0)
227
+ pids.add(pid);
228
+ }
229
+ }
230
+ for (const pid of pids) {
231
+ const alive = await isProcessRunning(pid);
232
+ if (alive) {
233
+ await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
234
+ console.log(`Killed process ${pid} using port ${port}`);
235
+ } else {
236
+ console.warn(`\u26A0 Port ${port} held by zombie PID ${pid} (process dead, socket stuck in kernel). Will clear on reboot or TCP timeout.`);
237
+ }
238
+ }
239
+ } else {
240
+ const result = await $`lsof -ti :${port}`.nothrow().text();
241
+ if (result.trim()) {
242
+ const pids = result.trim().split(`
243
+ `).filter((pid) => pid);
244
+ for (const pid of pids) {
245
+ await $`kill -9 ${pid}`.nothrow();
246
+ console.log(`Killed process ${pid} using port ${port}`);
247
+ }
248
+ }
249
+ }
250
+ } catch (error) {
251
+ console.warn(`Warning: Could not check or kill process on port ${port}: ${error}`);
252
+ }
253
+ }
254
+ function ensureDir(dirPath) {
255
+ if (!fs.existsSync(dirPath)) {
256
+ fs.mkdirSync(dirPath, { recursive: true });
257
+ }
258
+ }
259
+ function getShellCommand(command) {
260
+ if (isWindows()) {
261
+ return ["cmd", "/c", command];
262
+ } else {
263
+ return ["sh", "-c", command];
264
+ }
265
+ }
266
+ async function findChildPid(parentPid) {
267
+ let currentPid = parentPid;
268
+ const maxDepth = 2;
269
+ for (let depth = 0;depth < maxDepth; depth++) {
270
+ try {
271
+ let childPids = [];
272
+ if (isWindows()) {
273
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
274
+ childPids = result.split(`
275
+ `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
276
+ } else {
277
+ const result = await $`ps --no-headers -o pid --ppid ${currentPid}`.nothrow().text();
278
+ childPids = result.trim().split(`
279
+ `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
280
+ }
281
+ if (childPids.length === 0)
282
+ break;
283
+ currentPid = childPids[0];
284
+ } catch {
285
+ break;
286
+ }
287
+ }
288
+ return currentPid;
289
+ }
290
+ async function reconcileProcessPids(processes, deadPids) {
291
+ return await plat.measure("Reconcile PIDs", async () => {
292
+ const result = new Map;
293
+ const needsReconciliation = processes.filter((p) => deadPids.has(p.pid) && p.pid > 0);
294
+ if (needsReconciliation.length === 0)
295
+ return result;
296
+ try {
297
+ let runningProcs = [];
298
+ if (isWindows()) {
299
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`, 5000);
300
+ for (const line of output.split(`
301
+ `)) {
302
+ const sepIdx = line.indexOf("|");
303
+ if (sepIdx === -1)
304
+ continue;
305
+ const pid = parseInt(line.substring(0, sepIdx).trim());
306
+ const cmdLine = line.substring(sepIdx + 1).trim();
307
+ if (!isNaN(pid) && pid > 0 && cmdLine) {
308
+ runningProcs.push({ pid, cmdLine });
309
+ }
310
+ }
311
+ } else {
312
+ const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
313
+ for (const line of psOutput.trim().split(`
314
+ `)) {
315
+ const match = line.trim().match(/^(\d+)\s+(.+)/);
316
+ if (match) {
317
+ runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
318
+ }
319
+ }
320
+ }
321
+ for (const proc of needsReconciliation) {
322
+ const cmdParts = proc.command.split(/\s+/);
323
+ const workdirParts = proc.workdir.replace(/\\/g, "/").split("/").filter(Boolean);
324
+ const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || "";
325
+ let bestMatch = null;
326
+ let ambiguous = false;
327
+ for (const running of runningProcs) {
328
+ const cmdLower = running.cmdLine.toLowerCase();
329
+ let score = 0;
330
+ for (const part of cmdParts) {
331
+ if (part.length > 2 && cmdLower.includes(part.toLowerCase()))
332
+ score++;
333
+ }
334
+ if (workdirLast && cmdLower.includes(workdirLast))
335
+ score += 3;
336
+ if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, "/")))
337
+ score += 5;
338
+ if (cmdLower.includes(proc.workdir.toLowerCase()))
339
+ score += 5;
340
+ if (score < 4)
341
+ continue;
342
+ if (!bestMatch || score > bestMatch.score) {
343
+ ambiguous = false;
344
+ bestMatch = { pid: running.pid, score };
345
+ } else if (score === bestMatch.score) {
346
+ ambiguous = true;
347
+ }
348
+ }
349
+ if (bestMatch && !ambiguous) {
350
+ result.set(proc.name, bestMatch.pid);
351
+ runningProcs = runningProcs.filter((p) => p.pid !== bestMatch.pid);
352
+ }
353
+ }
354
+ } catch {}
355
+ return result;
356
+ }) ?? new Map;
357
+ }
358
+ async function findPidByPort(port, maxWaitMs = 8000) {
359
+ const start = Date.now();
360
+ const pollMs = 500;
361
+ while (Date.now() - start < maxWaitMs) {
362
+ try {
363
+ if (isWindows()) {
364
+ const result = await $`netstat -ano`.nothrow().quiet().text();
365
+ for (const line of result.split(`
366
+ `)) {
367
+ if (line.includes(`:${port}`) && line.includes("LISTENING")) {
368
+ const parts = line.trim().split(/\s+/);
369
+ const pid = parseInt(parts[parts.length - 1]);
370
+ if (!isNaN(pid) && pid > 0)
371
+ return pid;
372
+ }
373
+ }
374
+ } else {
375
+ try {
376
+ const result2 = await $`ss -tlnp`.nothrow().quiet().text();
377
+ for (const line of result2.split(`
378
+ `)) {
379
+ if (line.includes(`:${port}`)) {
380
+ const pidMatch = line.match(/pid=(\d+)/);
381
+ if (pidMatch)
382
+ return parseInt(pidMatch[1]);
383
+ }
384
+ }
385
+ } catch {}
386
+ const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
387
+ const pid = parseInt(result.trim());
388
+ if (!isNaN(pid) && pid > 0)
389
+ return pid;
390
+ }
391
+ } catch {}
392
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
393
+ }
394
+ return null;
395
+ }
396
+ async function readFileTail(filePath, lines) {
397
+ return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
398
+ try {
399
+ const content = await Bun.file(filePath).text();
400
+ if (!lines) {
401
+ return content;
402
+ }
403
+ const allLines = content.split(/\r?\n/);
404
+ const tailLines = allLines.slice(-lines);
405
+ return tailLines.join(`
406
+ `);
407
+ } catch (error) {
408
+ throw new Error(`Error reading file: ${error}`);
409
+ }
410
+ }) ?? "";
411
+ }
412
+ function copyFile(src, dest) {
413
+ fs.copyFileSync(src, dest);
414
+ }
415
+ async function getProcessMemory(pid) {
416
+ const map = await getProcessBatchResources([pid]);
417
+ return map.get(pid)?.memory || 0;
418
+ }
419
+ async function getProcessBatchResources(pids) {
420
+ if (pids.length === 0)
421
+ return new Map;
422
+ return await plat.measure(`Batch resources (${pids.length} PIDs)`, async () => {
423
+ const resourceMap = new Map;
424
+ const pidSet = new Set(pids);
425
+ try {
426
+ if (isWindows()) {
427
+ const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
428
+ for (const line of output.split(`
429
+ `)) {
430
+ const sepIdx = line.indexOf("|");
431
+ if (sepIdx === -1)
432
+ continue;
433
+ const pid = parseInt(line.substring(0, sepIdx).trim());
434
+ const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
435
+ if (!isNaN(pid) && pidSet.has(pid)) {
436
+ resourceMap.set(pid, { memory, cpu: 0 });
437
+ }
438
+ }
439
+ } else {
440
+ const result = await $`ps -eo pid,pcpu,rss`.nothrow().quiet().text();
441
+ const lines = result.trim().split(`
442
+ `);
443
+ for (let i = 1;i < lines.length; i++) {
444
+ const line = lines[i].trim();
445
+ if (!line)
446
+ continue;
447
+ const [pidStr, cpuStr, rssStr] = line.split(/\s+/);
448
+ const pid = parseInt(pidStr);
449
+ const cpu = parseFloat(cpuStr) || 0;
450
+ const rss = parseInt(rssStr) || 0;
451
+ if (pidSet.has(pid)) {
452
+ resourceMap.set(pid, { memory: rss * 1024, cpu });
453
+ }
454
+ }
455
+ }
456
+ } catch (e) {}
457
+ return resourceMap;
458
+ }) ?? new Map;
459
+ }
460
+ function parseUnixListeningPorts(output) {
461
+ const ports = new Set;
462
+ for (const line of output.split(`
463
+ `)) {
464
+ const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
465
+ if (portMatch) {
466
+ ports.add(parseInt(portMatch[1]));
467
+ }
468
+ }
469
+ return Array.from(ports);
470
+ }
471
+ async function getProcessPorts(pid) {
472
+ try {
473
+ if (isWindows()) {
474
+ const result = await $`netstat -ano`.nothrow().quiet().text();
475
+ const ports = new Set;
476
+ for (const line of result.split(`
477
+ `)) {
478
+ const match = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/);
479
+ if (match && parseInt(match[2]) === pid) {
480
+ ports.add(parseInt(match[1]));
481
+ }
482
+ }
483
+ return Array.from(ports);
484
+ } else {
485
+ try {
486
+ const result2 = await $`ss -tlnp`.nothrow().quiet().text();
487
+ const ports = new Set;
488
+ for (const line of result2.split(`
489
+ `)) {
490
+ if (line.includes(`pid=${pid}`)) {
491
+ const portMatch = line.match(/:(\d+)\s/);
492
+ if (portMatch) {
493
+ ports.add(parseInt(portMatch[1]));
494
+ }
495
+ }
496
+ }
497
+ if (ports.size > 0)
498
+ return Array.from(ports);
499
+ } catch {}
500
+ const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
501
+ return parseUnixListeningPorts(result);
502
+ }
503
+ } catch {
504
+ return [];
505
+ }
506
+ }
507
+ var plat;
508
+ var init_platform = __esm(() => {
509
+ plat = createMeasure("platform");
510
+ });
511
+
512
+ // src/db.ts
513
+ var exports_db = {};
514
+ __export(exports_db, {
515
+ updateProcessPid: () => updateProcessPid,
516
+ updateProcessEnv: () => updateProcessEnv,
517
+ saveTemplate: () => saveTemplate,
518
+ retryDatabaseOperation: () => retryDatabaseOperation,
519
+ removeProcessByName: () => removeProcessByName,
520
+ removeProcess: () => removeProcess,
521
+ removeDependency: () => removeDependency,
522
+ removeAllProcesses: () => removeAllProcesses,
523
+ removeAllDependencies: () => removeAllDependencies,
524
+ insertProcess: () => insertProcess,
525
+ getTemplate: () => getTemplate,
526
+ getStartOrder: () => getStartOrder,
527
+ getRecentHistory: () => getRecentHistory,
528
+ getProcessHistory: () => getProcessHistory,
529
+ getProcess: () => getProcess,
530
+ getDependents: () => getDependents,
531
+ getDependencyGraph: () => getDependencyGraph,
532
+ getDependencies: () => getDependencies,
533
+ getDbInfo: () => getDbInfo,
534
+ getAllTemplates: () => getAllTemplates,
535
+ getAllProcesses: () => getAllProcesses,
536
+ deleteTemplate: () => deleteTemplate,
537
+ dbPath: () => dbPath,
538
+ db: () => db,
539
+ clearOldHistory: () => clearOldHistory,
540
+ bgrHome: () => bgrHome,
541
+ addHistoryEntry: () => addHistoryEntry,
542
+ addDependency: () => addDependency,
543
+ TemplateSchema: () => TemplateSchema,
544
+ ProcessSchema: () => ProcessSchema,
545
+ HistorySchema: () => HistorySchema,
546
+ DependencySchema: () => DependencySchema
547
+ });
548
+ import { Database, z } from "sqlite-zod-orm";
549
+ import { join as join2 } from "path";
550
+ var {sleep } = globalThis.Bun;
551
+ import { existsSync as existsSync2, copyFileSync as copyFileSync2 } from "fs";
552
+ function shouldAutoMigrateLegacyDb() {
553
+ const raw = (process.env.BGRUN_DISABLE_LEGACY_MIGRATION || "").trim().toLowerCase();
554
+ return !(raw === "1" || raw === "true" || raw === "yes");
555
+ }
556
+ function getProcess(name) {
557
+ return db.process.select().where({ name }).orderBy("timestamp", "desc").limit(1).get() || null;
558
+ }
559
+ function getAllProcesses() {
560
+ return db.process.select().all();
561
+ }
562
+ function insertProcess(data) {
563
+ return db.process.insert({
564
+ ...data,
565
+ timestamp: new Date().toISOString()
566
+ });
567
+ }
568
+ function removeProcess(pid) {
569
+ const matches = db.process.select().where({ pid }).all();
570
+ for (const p of matches) {
571
+ db.process.delete(p.id);
572
+ }
573
+ }
574
+ function removeProcessByName(name) {
575
+ const matches = db.process.select().where({ name }).all();
576
+ for (const p of matches) {
577
+ db.process.delete(p.id);
578
+ }
579
+ }
580
+ function updateProcessPid(name, newPid) {
581
+ const proc = db.process.select().where({ name }).limit(1).get();
582
+ if (proc) {
583
+ db.process.update(proc.id, { pid: newPid });
584
+ }
585
+ }
586
+ function removeAllProcesses() {
587
+ const all = db.process.select().all();
588
+ for (const p of all) {
589
+ db.process.delete(p.id);
590
+ }
591
+ }
592
+ function updateProcessEnv(name, envJson) {
593
+ const proc = db.process.select().where({ name }).limit(1).get();
594
+ if (proc) {
595
+ db.process.update(proc.id, { env: envJson });
596
+ }
597
+ }
598
+ function getAllTemplates() {
599
+ return db.template.select().all();
600
+ }
601
+ function getTemplate(name) {
602
+ return db.template.select().where({ name }).limit(1).get() || null;
603
+ }
604
+ function saveTemplate(data) {
605
+ const existing = db.template.select().where({ name: data.name }).limit(1).get();
606
+ if (existing) {
607
+ db.template.update(existing.id, {
608
+ command: data.command,
609
+ workdir: data.workdir || "",
610
+ env: data.env || "",
611
+ group: data.group || ""
612
+ });
613
+ } else {
614
+ db.template.insert({
615
+ name: data.name,
616
+ command: data.command,
617
+ workdir: data.workdir || "",
618
+ env: data.env || "",
619
+ group: data.group || ""
620
+ });
621
+ }
622
+ }
623
+ function deleteTemplate(name) {
624
+ const tmpl = db.template.select().where({ name }).limit(1).get();
625
+ if (tmpl) {
626
+ db.template.delete(tmpl.id);
627
+ }
628
+ }
629
+ function getProcessHistory(name, limit = 50) {
630
+ return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
631
+ }
632
+ function addHistoryEntry(processName, event, pid, metadata = {}) {
633
+ return db.history.insert({
634
+ process_name: processName,
635
+ event,
636
+ pid,
637
+ metadata: JSON.stringify(metadata)
638
+ });
639
+ }
640
+ function getRecentHistory(limit = 100) {
641
+ return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
642
+ }
643
+ function clearOldHistory(daysToKeep = 30) {
644
+ const cutoff = new Date;
645
+ cutoff.setDate(cutoff.getDate() - daysToKeep);
646
+ const cutoffStr = cutoff.toISOString();
647
+ const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
648
+ for (const entry of oldEntries) {
649
+ db.history.delete(entry.id);
650
+ }
651
+ return oldEntries.length;
652
+ }
653
+ function getDependencies(processName) {
654
+ return db.dependency.select().where({ process_name: processName }).all().map((d) => d.depends_on);
655
+ }
656
+ function getDependents(processName) {
657
+ return db.dependency.select().where({ depends_on: processName }).all().map((d) => d.process_name);
658
+ }
659
+ function getDependencyGraph() {
660
+ const all = db.dependency.select().all();
661
+ const graph = {};
662
+ for (const dep of all) {
663
+ if (!graph[dep.process_name])
664
+ graph[dep.process_name] = [];
665
+ graph[dep.process_name].push(dep.depends_on);
666
+ }
667
+ return graph;
668
+ }
669
+ function addDependency(processName, dependsOn) {
670
+ if (processName === dependsOn)
671
+ return false;
672
+ const existing = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).limit(1).get();
673
+ if (existing)
674
+ return false;
675
+ if (wouldCreateCycle(processName, dependsOn))
676
+ return false;
677
+ db.dependency.insert({ process_name: processName, depends_on: dependsOn });
678
+ return true;
679
+ }
680
+ function removeDependency(processName, dependsOn) {
681
+ const matches = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).all();
682
+ for (const dep of matches) {
683
+ db.dependency.delete(dep.id);
684
+ }
685
+ }
686
+ function removeAllDependencies(processName) {
687
+ const matches = db.dependency.select().where({ process_name: processName }).all();
688
+ for (const dep of matches) {
689
+ db.dependency.delete(dep.id);
690
+ }
691
+ }
692
+ function wouldCreateCycle(processName, dependsOn) {
693
+ const graph = getDependencyGraph();
694
+ if (!graph[processName])
695
+ graph[processName] = [];
696
+ graph[processName].push(dependsOn);
697
+ const visited = new Set;
698
+ const stack = [dependsOn];
699
+ while (stack.length > 0) {
700
+ const current = stack.pop();
701
+ if (current === processName)
702
+ return true;
703
+ if (visited.has(current))
704
+ continue;
705
+ visited.add(current);
706
+ for (const dep of graph[current] || []) {
707
+ stack.push(dep);
708
+ }
709
+ }
710
+ return false;
711
+ }
712
+ function getStartOrder() {
713
+ const graph = getDependencyGraph();
714
+ const allProcesses = getAllProcesses().map((p) => p.name);
715
+ const allNames = new Set(allProcesses);
716
+ const inDegree = {};
717
+ for (const name of allNames)
718
+ inDegree[name] = 0;
719
+ for (const [proc, deps] of Object.entries(graph)) {
720
+ for (const dep of deps) {
721
+ if (allNames.has(dep)) {
722
+ inDegree[proc] = (inDegree[proc] || 0) + 1;
723
+ }
724
+ }
725
+ }
726
+ const queue = [];
727
+ for (const name of allNames) {
728
+ if ((inDegree[name] || 0) === 0)
729
+ queue.push(name);
730
+ }
731
+ const order = [];
732
+ while (queue.length > 0) {
733
+ queue.sort();
734
+ const current = queue.shift();
735
+ order.push(current);
736
+ for (const [proc, deps] of Object.entries(graph)) {
737
+ if (deps.includes(current) && allNames.has(proc)) {
738
+ inDegree[proc]--;
739
+ if (inDegree[proc] === 0)
740
+ queue.push(proc);
741
+ }
742
+ }
743
+ }
744
+ return order;
745
+ }
746
+ function getDbInfo() {
747
+ return {
748
+ dbPath,
749
+ bgrHome,
750
+ dbFilename,
751
+ exists: existsSync2(dbPath)
752
+ };
753
+ }
754
+ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
755
+ for (let attempt = 1;attempt <= maxRetries; attempt++) {
756
+ try {
757
+ return operation();
758
+ } catch (err) {
759
+ if (err?.code === "SQLITE_BUSY" && attempt < maxRetries) {
760
+ await sleep(delay * attempt);
761
+ continue;
762
+ }
763
+ throw err;
764
+ }
765
+ }
766
+ throw new Error("Max retries reached for database operation");
767
+ }
768
+ var ProcessSchema, TemplateSchema, HistorySchema, DependencySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
769
+ var init_db = __esm(() => {
770
+ init_platform();
771
+ ProcessSchema = z.object({
772
+ pid: z.number(),
773
+ workdir: z.string(),
774
+ command: z.string(),
775
+ name: z.string(),
776
+ env: z.string(),
777
+ configPath: z.string().default(""),
778
+ stdout_path: z.string(),
779
+ stderr_path: z.string(),
780
+ timestamp: z.string().default(() => new Date().toISOString()),
781
+ group: z.string().default("")
782
+ });
783
+ TemplateSchema = z.object({
784
+ name: z.string(),
785
+ command: z.string(),
786
+ workdir: z.string().default(""),
787
+ env: z.string().default(""),
788
+ group: z.string().default(""),
789
+ created_at: z.string().default(() => new Date().toISOString())
790
+ });
791
+ HistorySchema = z.object({
792
+ process_name: z.string(),
793
+ event: z.string(),
794
+ pid: z.number().optional(),
795
+ timestamp: z.string().default(() => new Date().toISOString()),
796
+ metadata: z.string().default("")
797
+ });
798
+ DependencySchema = z.object({
799
+ process_name: z.string(),
800
+ depends_on: z.string(),
801
+ created_at: z.string().default(() => new Date().toISOString())
802
+ });
803
+ homePath = getHomeDir();
804
+ bgrDir = join2(homePath, ".bgr");
805
+ ensureDir(bgrDir);
806
+ dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
807
+ dbPath = join2(bgrDir, dbFilename);
808
+ bgrHome = bgrDir;
809
+ legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
810
+ if (shouldAutoMigrateLegacyDb() && !existsSync2(dbPath) && existsSync2(legacyDbPath)) {
811
+ try {
812
+ copyFileSync2(legacyDbPath, dbPath);
813
+ console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
814
+ } catch (e) {}
815
+ }
816
+ db = new Database(dbPath, {
817
+ process: ProcessSchema,
818
+ template: TemplateSchema,
819
+ history: HistorySchema,
820
+ dependency: DependencySchema
821
+ }, {
822
+ indexes: {
823
+ process: ["name", "timestamp", "pid"],
824
+ template: ["name"],
825
+ history: ["process_name", "timestamp"],
826
+ dependency: ["process_name", "depends_on"]
827
+ }
828
+ });
829
+ });
830
+
831
+ // src/utils.ts
832
+ import * as fs2 from "fs";
833
+ function parseEnvString(envString) {
834
+ const env = {};
835
+ envString.split(",").forEach((pair) => {
836
+ const [key, value] = pair.split("=");
837
+ if (key && value)
838
+ env[key] = value;
839
+ });
840
+ return env;
841
+ }
842
+ function calculateRuntime(startTime) {
843
+ const start = new Date(startTime).getTime();
844
+ const now = new Date().getTime();
845
+ const diffInMinutes = Math.floor((now - start) / (1000 * 60));
846
+ return `${diffInMinutes} minutes`;
847
+ }
848
+ async function getVersion() {
849
+ try {
850
+ const { join: join3 } = await import("path");
851
+ const pkgPath = join3(import.meta.dir, "../package.json");
852
+ const pkg = await Bun.file(pkgPath).json();
853
+ return pkg.version || "0.0.0";
854
+ } catch {
855
+ return "0.0.0";
856
+ }
857
+ }
858
+ function validateDirectory(directory) {
859
+ if (!directory || !fs2.existsSync(directory)) {
860
+ throw new Error(`Directory not found or invalid: '${directory}'`);
861
+ }
862
+ }
863
+ function tailFile(path, prefix, colorFn, lines) {
864
+ let position = 0;
865
+ let lastPartial = "";
866
+ if (!fs2.existsSync(path)) {
867
+ return () => {};
868
+ }
869
+ const fd = fs2.openSync(path, "r");
870
+ const printNewContent = () => {
871
+ try {
872
+ const stats = fs2.statSync(path);
873
+ if (stats.size <= position)
874
+ return;
875
+ const buffer = Buffer.alloc(stats.size - position);
876
+ fs2.readSync(fd, buffer, 0, buffer.length, position);
877
+ let content = buffer.toString();
878
+ content = lastPartial + content;
879
+ lastPartial = "";
880
+ const lineArray = content.split(/\r?\n/);
881
+ if (!content.endsWith(`
882
+ `)) {
883
+ lastPartial = lineArray.pop() || "";
884
+ }
885
+ lineArray.forEach((line) => {
886
+ if (line) {
887
+ console.log(colorFn(prefix + line));
888
+ }
889
+ });
890
+ position = stats.size;
891
+ } catch (e) {}
892
+ };
893
+ const watcher = fs2.watch(path, { persistent: true }, (event) => {
894
+ if (event === "change") {
895
+ printNewContent();
896
+ }
897
+ });
898
+ printNewContent();
899
+ return () => {
900
+ watcher.close();
901
+ try {
902
+ fs2.closeSync(fd);
903
+ } catch {}
904
+ };
905
+ }
906
+ var init_utils = __esm(() => {
907
+ init_platform();
908
+ });
909
+
910
+ // src/deps.ts
911
+ var exports_deps = {};
912
+ __export(exports_deps, {
913
+ getUnmetDeps: () => getUnmetDeps,
914
+ getDependencies: () => getDependencies2,
915
+ buildDepGraph: () => buildDepGraph
916
+ });
917
+ function getDependencies2(envStr) {
918
+ const env = parseEnvString(envStr);
919
+ const raw = env.BGR_DEPENDS_ON || "";
920
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
921
+ }
922
+ async function buildDepGraph() {
923
+ const processes = getAllProcesses();
924
+ const nodeMap = new Map;
925
+ for (const proc of processes) {
926
+ const deps = getDependencies2(proc.env);
927
+ const alive = await isProcessRunning(proc.pid, proc.command);
928
+ nodeMap.set(proc.name, {
929
+ name: proc.name,
930
+ dependsOn: deps,
931
+ dependedBy: [],
932
+ running: alive,
933
+ pid: proc.pid
934
+ });
935
+ }
936
+ for (const node of nodeMap.values()) {
937
+ for (const dep of node.dependsOn) {
938
+ const depNode = nodeMap.get(dep);
939
+ if (depNode) {
940
+ depNode.dependedBy.push(node.name);
941
+ }
942
+ }
943
+ }
944
+ const inDegree = new Map;
945
+ for (const node of nodeMap.values()) {
946
+ inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
947
+ }
948
+ const queue = [];
949
+ for (const [name, degree] of inDegree) {
950
+ if (degree === 0)
951
+ queue.push(name);
952
+ }
953
+ const order = [];
954
+ while (queue.length > 0) {
955
+ const current = queue.shift();
956
+ order.push(current);
957
+ const node = nodeMap.get(current);
958
+ for (const dependent of node.dependedBy) {
959
+ const newDegree = (inDegree.get(dependent) || 0) - 1;
960
+ inDegree.set(dependent, newDegree);
961
+ if (newDegree === 0)
962
+ queue.push(dependent);
963
+ }
964
+ }
965
+ const hasCycle = order.length < nodeMap.size;
966
+ const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
967
+ return {
968
+ nodes: [...nodeMap.values()],
969
+ order,
970
+ hasCycle,
971
+ cycleNodes
972
+ };
973
+ }
974
+ async function getUnmetDeps(name) {
975
+ const proc = getProcess(name);
976
+ if (!proc)
977
+ return [];
978
+ const deps = getDependencies2(proc.env);
979
+ const unmet = [];
980
+ for (const depName of deps) {
981
+ const depProc = getProcess(depName);
982
+ if (!depProc) {
983
+ unmet.push(depName);
984
+ continue;
985
+ }
986
+ const alive = await isProcessRunning(depProc.pid, depProc.command);
987
+ if (!alive) {
988
+ unmet.push(depName);
989
+ }
990
+ }
991
+ return unmet;
992
+ }
993
+ var init_deps = __esm(() => {
994
+ init_db();
995
+ init_platform();
996
+ init_utils();
997
+ });
998
+
999
+ // src/log-rotation.ts
1000
+ var exports_log_rotation = {};
1001
+ __export(exports_log_rotation, {
1002
+ startLogRotation: () => startLogRotation,
1003
+ rotateLogFile: () => rotateLogFile,
1004
+ rotateAllLogs: () => rotateAllLogs
1005
+ });
1006
+ import { existsSync as existsSync4, statSync as statSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
1007
+ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1008
+ try {
1009
+ if (!existsSync4(filePath))
1010
+ return false;
1011
+ const stat = statSync2(filePath);
1012
+ if (stat.size <= maxBytes)
1013
+ return false;
1014
+ const content = readFileSync(filePath, "utf-8");
1015
+ const lines = content.split(`
1016
+ `);
1017
+ if (lines.length <= keepLines)
1018
+ return false;
1019
+ const truncated = lines.slice(-keepLines);
1020
+ const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
1021
+ `;
1022
+ writeFileSync2(filePath, header + truncated.join(`
1023
+ `));
1024
+ return true;
1025
+ } catch {
1026
+ return false;
1027
+ }
1028
+ }
1029
+ function rotateAllLogs(getProcesses, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1030
+ const processes = getProcesses();
1031
+ const rotated = [];
1032
+ let checked = 0;
1033
+ for (const proc of processes) {
1034
+ if (proc.stdout_path) {
1035
+ checked++;
1036
+ if (rotateLogFile(proc.stdout_path, maxBytes, keepLines)) {
1037
+ rotated.push(`${proc.name}/stdout`);
1038
+ }
1039
+ }
1040
+ if (proc.stderr_path) {
1041
+ checked++;
1042
+ if (rotateLogFile(proc.stderr_path, maxBytes, keepLines)) {
1043
+ rotated.push(`${proc.name}/stderr`);
1044
+ }
1045
+ }
1046
+ }
1047
+ return { rotated, checked };
1048
+ }
1049
+ function startLogRotation(getProcesses, intervalMs = DEFAULT_CHECK_INTERVAL_MS, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
1050
+ console.log(`[logs] Log rotation active: max ${formatBytes(maxBytes)}/file, keep ${keepLines} lines, check every ${intervalMs / 1000}s`);
1051
+ return setInterval(() => {
1052
+ const { rotated } = rotateAllLogs(getProcesses, maxBytes, keepLines);
1053
+ if (rotated.length > 0) {
1054
+ console.log(`[logs] Rotated ${rotated.length} log(s): ${rotated.join(", ")}`);
1055
+ }
1056
+ }, intervalMs);
1057
+ }
1058
+ function formatBytes(bytes) {
1059
+ if (bytes >= 1e6)
1060
+ return `${(bytes / 1e6).toFixed(1)}MB`;
1061
+ if (bytes >= 1000)
1062
+ return `${(bytes / 1000).toFixed(0)}KB`;
1063
+ return `${bytes}B`;
1064
+ }
1065
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP_LINES = 5000, DEFAULT_CHECK_INTERVAL_MS = 60000;
1066
+ var init_log_rotation = __esm(() => {
1067
+ DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
1068
+ });
1069
+
1070
+ // src/logger.ts
1071
+ import boxen from "boxen";
1072
+ import chalk from "chalk";
1073
+ function announce(message, title) {
1074
+ console.log(boxen(message, {
1075
+ padding: 1,
1076
+ margin: 1,
1077
+ borderColor: "green",
1078
+ title: title || "bgrun",
1079
+ titleAlignment: "center",
1080
+ borderStyle: "round"
1081
+ }));
1082
+ }
1083
+ function error(message) {
1084
+ const text = message instanceof Error ? message.stack || message.message : String(message);
1085
+ console.error(boxen(chalk.red(text), {
1086
+ padding: 1,
1087
+ margin: 1,
1088
+ borderColor: "red",
1089
+ title: "Error",
1090
+ titleAlignment: "center",
1091
+ borderStyle: "double"
1092
+ }));
1093
+ throw new BgrunError(text);
1094
+ }
1095
+ var BgrunError;
1096
+ var init_logger = __esm(() => {
1097
+ BgrunError = class BgrunError extends Error {
1098
+ constructor(message) {
1099
+ super(message);
1100
+ this.name = "BgrunError";
1101
+ }
1102
+ };
1103
+ });
1104
+
1105
+ // src/config.ts
1106
+ function formatEnvKey(key) {
1107
+ return key.toUpperCase().replace(/\./g, "_");
1108
+ }
1109
+ function flattenConfig(obj, prefix = "") {
1110
+ return Object.keys(obj).reduce((acc, key) => {
1111
+ const value = obj[key];
1112
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
1113
+ if (Array.isArray(value)) {
1114
+ value.forEach((item, index) => {
1115
+ const indexedPrefix = `${newPrefix}.${index}`;
1116
+ if (typeof item === "object" && item !== null) {
1117
+ Object.assign(acc, flattenConfig(item, indexedPrefix));
1118
+ } else {
1119
+ acc[formatEnvKey(indexedPrefix)] = String(item);
1120
+ }
1121
+ });
1122
+ } else if (typeof value === "object" && value !== null) {
1123
+ Object.assign(acc, flattenConfig(value, newPrefix));
1124
+ } else {
1125
+ acc[formatEnvKey(newPrefix)] = String(value);
1126
+ }
1127
+ return acc;
1128
+ }, {});
1129
+ }
1130
+ async function parseConfigFile(configPath) {
1131
+ const importPath = `${configPath}?t=${Date.now()}`;
1132
+ const parsedConfig = await import(importPath).then((m) => m.default);
1133
+ return flattenConfig(parsedConfig);
1134
+ }
1135
+
1136
+ // src/commands/run.ts
1137
+ var {$: $2 } = globalThis.Bun;
1138
+ var {sleep: sleep2 } = globalThis.Bun;
1139
+ import { join as join3 } from "path";
1140
+ import { createMeasure as createMeasure2 } from "measure-fn";
1141
+ async function handleRun(options) {
1142
+ const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
1143
+ const existingProcess = name ? getProcess(name) : null;
1144
+ if (name && existingProcess) {
1145
+ const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1146
+ const unmet = await getUnmetDeps2(name);
1147
+ if (unmet.length > 0) {
1148
+ await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1149
+ for (const depName of unmet) {
1150
+ const depProc = getProcess(depName);
1151
+ if (depProc) {
1152
+ announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1153
+ await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
1154
+ }
1155
+ }
1156
+ });
1157
+ }
1158
+ }
1159
+ if (existingProcess) {
1160
+ const finalDirectory2 = directory || existingProcess.workdir;
1161
+ validateDirectory(finalDirectory2);
1162
+ $2.cwd(finalDirectory2);
1163
+ if (fetch) {
1164
+ if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1165
+ error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1166
+ }
1167
+ await run.measure(`Git fetch "${name}"`, async () => {
1168
+ try {
1169
+ await $2`git fetch origin`;
1170
+ const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1171
+ const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1172
+ if (localHash !== remoteHash) {
1173
+ await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1174
+ announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1175
+ }
1176
+ } catch (err) {
1177
+ error(`Failed to pull latest changes: ${err}`);
1178
+ }
1179
+ });
1180
+ }
1181
+ const isRunning = await isProcessRunning(existingProcess.pid);
1182
+ if (isRunning && !force) {
1183
+ error(`Process '${name}' is currently running. Use --force to restart.`);
1184
+ }
1185
+ let detectedPorts = [];
1186
+ if (isRunning) {
1187
+ detectedPorts = await getProcessPorts(existingProcess.pid);
1188
+ }
1189
+ if (isRunning) {
1190
+ await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
1191
+ await terminateProcess(existingProcess.pid);
1192
+ announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1193
+ });
1194
+ }
1195
+ if (detectedPorts.length > 0) {
1196
+ await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1197
+ for (const port of detectedPorts) {
1198
+ await killProcessOnPort(port);
1199
+ }
1200
+ for (const port of detectedPorts) {
1201
+ const freed = await waitForPortFree(port, 5000);
1202
+ if (!freed) {
1203
+ await killProcessOnPort(port);
1204
+ await waitForPortFree(port, 3000);
1205
+ }
1206
+ }
1207
+ });
1208
+ }
1209
+ const cmdToMatch = existingProcess.command;
1210
+ if (cmdToMatch) {
1211
+ await run.measure("Zombie sweep", async () => {
1212
+ try {
1213
+ const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1214
+ const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1215
+ if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1216
+ return;
1217
+ }
1218
+ const currentPid = process.pid;
1219
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
1220
+ const zombiePids = result.split(`
1221
+ `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1222
+ for (const zPid of zombiePids) {
1223
+ await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1224
+ }
1225
+ if (zombiePids.length > 0) {
1226
+ announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1227
+ }
1228
+ } catch {}
1229
+ });
1230
+ }
1231
+ await retryDatabaseOperation(() => removeProcessByName(name));
1232
+ } else {
1233
+ if (!directory || !name || !command) {
1234
+ error("'directory', 'name', and 'command' parameters are required for new processes.");
1235
+ }
1236
+ validateDirectory(directory);
1237
+ $2.cwd(directory);
1238
+ }
1239
+ const finalCommand = command || existingProcess.command;
1240
+ const finalDirectory = directory || existingProcess?.workdir;
1241
+ let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1242
+ if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1243
+ finalEnv.BGR_KEEP_ALIVE = "true";
1244
+ }
1245
+ let finalConfigPath;
1246
+ if (configPath !== undefined) {
1247
+ finalConfigPath = configPath;
1248
+ } else if (existingProcess) {
1249
+ finalConfigPath = existingProcess.configPath;
1250
+ } else {
1251
+ finalConfigPath = ".config.toml";
1252
+ }
1253
+ if (finalConfigPath) {
1254
+ const fullConfigPath = join3(finalDirectory, finalConfigPath);
1255
+ if (await Bun.file(fullConfigPath).exists()) {
1256
+ const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1257
+ try {
1258
+ return await parseConfigFile(fullConfigPath);
1259
+ } catch (err) {
1260
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1261
+ return null;
1262
+ }
1263
+ });
1264
+ if (configEnv) {
1265
+ finalEnv = { ...finalEnv, ...configEnv };
1266
+ console.log(`Loaded config from ${finalConfigPath}`);
1267
+ }
1268
+ } else {
1269
+ console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1270
+ }
1271
+ }
1272
+ const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
1273
+ Bun.write(stdoutPath, "");
1274
+ const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
1275
+ Bun.write(stderrPath, "");
1276
+ const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1277
+ const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1278
+ env: { ...Bun.env, ...finalEnv },
1279
+ cwd: finalDirectory,
1280
+ stdout: Bun.file(stdoutPath),
1281
+ stderr: Bun.file(stderrPath)
1282
+ });
1283
+ newProcess.unref();
1284
+ await sleep2(100);
1285
+ const pid = await findChildPid(newProcess.pid);
1286
+ await sleep2(400);
1287
+ return pid;
1288
+ }) ?? 0;
1289
+ await retryDatabaseOperation(() => insertProcess({
1290
+ pid: actualPid,
1291
+ workdir: finalDirectory,
1292
+ command: finalCommand,
1293
+ name,
1294
+ env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
1295
+ configPath: finalConfigPath || "",
1296
+ stdout_path: stdoutPath,
1297
+ stderr_path: stderrPath
1298
+ }));
1299
+ announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1300
+ }
1301
+ var homePath2, run;
1302
+ var init_run = __esm(() => {
1303
+ init_db();
1304
+ init_platform();
1305
+ init_logger();
1306
+ init_utils();
1307
+ homePath2 = getHomeDir();
1308
+ run = createMeasure2("run");
1309
+ });
1310
+
1311
+ // src/server.ts
1312
+ var exports_server = {};
1313
+ __export(exports_server, {
1314
+ startServer: () => startServer,
1315
+ guardRestartCounts: () => guardRestartCounts,
1316
+ guardEvents: () => guardEvents
1317
+ });
1318
+ import path from "path";
1319
+ async function cleanupPort(port) {
1320
+ if (process.platform !== "win32")
1321
+ return port;
1322
+ try {
1323
+ const proc = Bun.spawn([
1324
+ "powershell",
1325
+ "-NoProfile",
1326
+ "-Command",
1327
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1328
+ ], { stdout: "pipe", stderr: "pipe" });
1329
+ const text = await new Response(proc.stdout).text();
1330
+ const pid = parseInt(text.trim(), 10);
1331
+ if (!pid || pid === process.pid)
1332
+ return port;
1333
+ const checkProc = Bun.spawn([
1334
+ "powershell",
1335
+ "-NoProfile",
1336
+ "-Command",
1337
+ `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
1338
+ ], { stdout: "pipe", stderr: "pipe" });
1339
+ const checkText = await new Response(checkProc.stdout).text();
1340
+ if (checkText.trim()) {
1341
+ console.log(`[server] Killing PID ${pid} holding port ${port}`);
1342
+ Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
1343
+ await Bun.sleep(1000);
1344
+ return port;
1345
+ } else {
1346
+ const fallback = port + 1;
1347
+ console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
1348
+ return fallback;
1349
+ }
1350
+ } catch {
1351
+ return port;
1352
+ }
1353
+ }
1354
+ async function startServer() {
1355
+ const { start } = await import("melina");
1356
+ const appDir = path.join(import.meta.dir, "../dashboard/app");
1357
+ const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
1358
+ _originalPort = requestedPort;
1359
+ const resolvedPort = await cleanupPort(requestedPort);
1360
+ _currentPort = resolvedPort;
1361
+ const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
1362
+ await start({
1363
+ appDir,
1364
+ defaultTitle: "bgrun Dashboard - Process Manager",
1365
+ globalCss: path.join(appDir, "globals.css"),
1366
+ ...needsExplicitPort && { port: resolvedPort }
1367
+ });
1368
+ startGuard();
1369
+ const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1370
+ startLogRotation2(() => getAllProcesses());
1371
+ if (resolvedPort !== requestedPort) {
1372
+ startStickyPortChecker();
1373
+ }
1374
+ }
1375
+ function startStickyPortChecker() {
1376
+ const CHECK_INTERVAL_MS = 60000;
1377
+ console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
1378
+ setInterval(async () => {
1379
+ if (_currentPort === _originalPort)
1380
+ return;
1381
+ try {
1382
+ const proc = Bun.spawn([
1383
+ "powershell",
1384
+ "-NoProfile",
1385
+ "-Command",
1386
+ `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1387
+ ], { stdout: "pipe", stderr: "pipe" });
1388
+ const text = await new Response(proc.stdout).text();
1389
+ const pid = parseInt(text.trim(), 10);
1390
+ if (!pid) {
1391
+ console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
1392
+ _currentPort = _originalPort;
1393
+ }
1394
+ } catch {}
1395
+ }, CHECK_INTERVAL_MS);
1396
+ }
1397
+ function startGuard() {
1398
+ console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
1399
+ setInterval(async () => {
1400
+ try {
1401
+ const processes = getAllProcesses();
1402
+ if (processes.length === 0)
1403
+ return;
1404
+ for (const proc of processes) {
1405
+ if (GUARD_SKIP_NAMES.has(proc.name))
1406
+ continue;
1407
+ const env = proc.env ? parseEnvString(proc.env) : {};
1408
+ if (env.BGR_KEEP_ALIVE !== "true")
1409
+ continue;
1410
+ const alive = await isProcessRunning(proc.pid, proc.command);
1411
+ if (!alive) {
1412
+ const now = Date.now();
1413
+ const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1414
+ if (now < nextRestart)
1415
+ continue;
1416
+ console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1417
+ let success = false;
1418
+ try {
1419
+ await handleRun({
1420
+ action: "run",
1421
+ name: proc.name,
1422
+ force: true,
1423
+ remoteName: ""
1424
+ });
1425
+ success = true;
1426
+ const prevCount = guardRestartCounts.get(proc.name) || 0;
1427
+ const newCount = prevCount + 1;
1428
+ guardRestartCounts.set(proc.name, newCount);
1429
+ try {
1430
+ addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1431
+ } catch {}
1432
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1433
+ if (guardEvents.length > 100)
1434
+ guardEvents.pop();
1435
+ if (newCount > 5) {
1436
+ const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
1437
+ guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
1438
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
1439
+ } else {
1440
+ console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
1441
+ }
1442
+ } catch (err) {
1443
+ console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1444
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1445
+ if (guardEvents.length > 100)
1446
+ guardEvents.pop();
1447
+ }
1448
+ } else {
1449
+ const prevCount = guardRestartCounts.get(proc.name) || 0;
1450
+ if (prevCount > 0) {
1451
+ const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1452
+ if (Date.now() > nextRestart + 60000) {
1453
+ guardRestartCounts.delete(proc.name);
1454
+ guardNextRestartTime.delete(proc.name);
1455
+ }
1456
+ }
1457
+ }
1458
+ }
1459
+ } catch (err) {
1460
+ console.error(`[guard] Error in guard loop: ${err.message}`);
1461
+ }
1462
+ }, GUARD_INTERVAL_MS);
1463
+ }
1464
+ var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
1465
+ var init_server = __esm(() => {
1466
+ init_db();
1467
+ init_platform();
1468
+ init_run();
1469
+ init_utils();
1470
+ GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1471
+ _g = globalThis;
1472
+ if (!_g.__bgrGuardRestartCounts)
1473
+ _g.__bgrGuardRestartCounts = new Map;
1474
+ if (!_g.__bgrGuardNextRestartTime)
1475
+ _g.__bgrGuardNextRestartTime = new Map;
1476
+ if (!_g.__bgrGuardEvents)
1477
+ _g.__bgrGuardEvents = [];
1478
+ guardRestartCounts = _g.__bgrGuardRestartCounts;
1479
+ guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1480
+ guardEvents = _g.__bgrGuardEvents;
1481
+ });
1482
+ init_server();
1483
+
1484
+ export {
1485
+ startServer,
1486
+ guardRestartCounts,
1487
+ guardEvents
1488
+ };