bgrun 3.12.11 → 3.12.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dashboard/app/api/config/[name]/route.ts +1 -1
  3. package/dashboard/app/api/debug/route.ts +1 -1
  4. package/dashboard/app/api/dependencies/route.ts +40 -40
  5. package/dashboard/app/api/deploy/[name]/route.ts +1 -1
  6. package/dashboard/app/api/deploy-all/route.ts +25 -25
  7. package/dashboard/app/api/deps/route.ts +3 -3
  8. package/dashboard/app/api/guard/route.ts +1 -1
  9. package/dashboard/app/api/guard-all/route.ts +1 -1
  10. package/dashboard/app/api/guard-events/route.ts +4 -4
  11. package/dashboard/app/api/history/route.ts +105 -105
  12. package/dashboard/app/api/logs/[name]/route.ts +100 -100
  13. package/dashboard/app/api/logs/rotate/route.ts +2 -2
  14. package/dashboard/app/api/next-port/route.ts +32 -32
  15. package/dashboard/app/api/processes/[name]/route.ts +2 -2
  16. package/dashboard/app/api/processes/route.ts +4 -4
  17. package/dashboard/app/api/restart/[name]/route.ts +2 -2
  18. package/dashboard/app/api/start/route.ts +2 -2
  19. package/dashboard/app/api/stop/[name]/route.ts +2 -2
  20. package/dashboard/app/api/templates/route.ts +46 -46
  21. package/dashboard/app/api/version/route.ts +1 -1
  22. package/dashboard/lib/runtime.ts +49 -0
  23. package/dist/api.js +94 -67
  24. package/dist/deploy.js +1373 -0
  25. package/dist/deps.js +1004 -0
  26. package/dist/index.js +224 -224
  27. package/dist/log-rotation.js +95 -0
  28. package/dist/server.js +1488 -0
  29. package/package.json +2 -17
  30. package/src/api.ts +0 -63
  31. package/src/build.ts +0 -24
  32. package/src/commands/cleanup.ts +0 -141
  33. package/src/commands/details.ts +0 -60
  34. package/src/commands/list.ts +0 -133
  35. package/src/commands/logs.ts +0 -49
  36. package/src/commands/run.ts +0 -217
  37. package/src/commands/watch.ts +0 -223
  38. package/src/config.ts +0 -37
  39. package/src/db.ts +0 -422
  40. package/src/deploy.ts +0 -163
  41. package/src/deps.ts +0 -126
  42. package/src/guard.ts +0 -208
  43. package/src/index.ts +0 -623
  44. package/src/log-rotation.ts +0 -93
  45. package/src/logger.ts +0 -40
  46. package/src/platform.ts +0 -665
  47. package/src/server.ts +0 -217
  48. package/src/table.ts +0 -232
  49. package/src/types.ts +0 -14
  50. package/src/utils.ts +0 -96
package/dist/deps.js ADDED
@@ -0,0 +1,1004 @@
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
+ init_deps();
999
+
1000
+ export {
1001
+ getUnmetDeps,
1002
+ getDependencies2 as getDependencies,
1003
+ buildDepGraph
1004
+ };