bgrun 3.12.0 → 3.12.2

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/index.js CHANGED
@@ -1,23 +1,66 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
18
  var __require = import.meta.require;
15
19
 
16
20
  // src/platform.ts
21
+ var exports_platform = {};
22
+ __export(exports_platform, {
23
+ waitForPortFree: () => waitForPortFree,
24
+ terminateProcess: () => terminateProcess,
25
+ reconcileProcessPids: () => reconcileProcessPids,
26
+ readFileTail: () => readFileTail,
27
+ psExec: () => psExec,
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
+ });
17
43
  import * as fs from "fs";
18
44
  import * as os from "os";
45
+ import { join } from "path";
19
46
  var {$ } = globalThis.Bun;
20
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
+ }
21
64
  function isWindows() {
22
65
  return process.platform === "win32";
23
66
  }
@@ -33,8 +76,13 @@ async function isProcessRunning(pid, command) {
33
76
  return await isDockerContainerRunning(command);
34
77
  }
35
78
  if (isWindows()) {
36
- const result = await $`tasklist /FI "PID eq ${pid}" /NH`.nothrow().text();
37
- return result.includes(`${pid}`);
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
+ }
38
86
  } else {
39
87
  const result = await $`ps -p ${pid}`.nothrow().text();
40
88
  return result.includes(`${pid}`);
@@ -66,7 +114,7 @@ async function isDockerContainerRunning(command) {
66
114
  async function getChildPids(pid) {
67
115
  try {
68
116
  if (isWindows()) {
69
- const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
117
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
70
118
  return result.split(`
71
119
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
72
120
  } else {
@@ -125,6 +173,35 @@ async function isPortFree(port) {
125
173
  return true;
126
174
  }
127
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
+ }
128
205
  async function waitForPortFree(port, timeoutMs = 5000) {
129
206
  const startTime = Date.now();
130
207
  const pollInterval = 300;
@@ -188,12 +265,12 @@ function getShellCommand(command) {
188
265
  }
189
266
  async function findChildPid(parentPid) {
190
267
  let currentPid = parentPid;
191
- const maxDepth = 5;
268
+ const maxDepth = 2;
192
269
  for (let depth = 0;depth < maxDepth; depth++) {
193
270
  try {
194
271
  let childPids = [];
195
272
  if (isWindows()) {
196
- const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
273
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
197
274
  childPids = result.split(`
198
275
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
199
276
  } else {
@@ -201,9 +278,8 @@ async function findChildPid(parentPid) {
201
278
  childPids = result.trim().split(`
202
279
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
203
280
  }
204
- if (childPids.length === 0) {
281
+ if (childPids.length === 0)
205
282
  break;
206
- }
207
283
  currentPid = childPids[0];
208
284
  } catch {
209
285
  break;
@@ -211,6 +287,112 @@ async function findChildPid(parentPid) {
211
287
  }
212
288
  return currentPid;
213
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
+ }
214
396
  async function readFileTail(filePath, lines) {
215
397
  return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
216
398
  try {
@@ -227,6 +409,13 @@ async function readFileTail(filePath, lines) {
227
409
  }
228
410
  }) ?? "";
229
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
+ }
230
419
  async function getProcessBatchResources(pids) {
231
420
  if (pids.length === 0)
232
421
  return new Map;
@@ -235,28 +424,16 @@ async function getProcessBatchResources(pids) {
235
424
  const pidSet = new Set(pids);
236
425
  try {
237
426
  if (isWindows()) {
238
- const result = await $`powershell -Command "Get-Process | Select-Object Id, CPU, WorkingSet"`.nothrow().quiet().text();
239
- const lines = result.trim().split(`
240
- `);
241
- for (const line of lines) {
242
- const trimmed = line.trim();
243
- if (!trimmed || trimmed.startsWith("Id") || trimmed.startsWith("--"))
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)
244
432
  continue;
245
- const parts = trimmed.split(/\s+/);
246
- if (parts.length >= 3) {
247
- const pid = parseInt(parts[0]);
248
- let cpuStr = parts[1];
249
- let memStr = parts[2];
250
- if (parts.length === 2) {
251
- cpuStr = "0";
252
- memStr = parts[1];
253
- }
254
- const cpu = parseFloat(cpuStr) || 0;
255
- const memory = parseInt(memStr) || 0;
256
- if (!isNaN(pid) && !isNaN(memory)) {
257
- if (pidSet.has(pid))
258
- resourceMap.set(pid, { memory, cpu });
259
- }
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 });
260
437
  }
261
438
  }
262
439
  } else {
@@ -309,15 +486,13 @@ async function getProcessPorts(pid) {
309
486
  if (ports2.size > 0)
310
487
  return Array.from(ports2);
311
488
  } catch {}
312
- const result = await $`lsof -i -P -n -p ${pid}`.nothrow().quiet().text();
489
+ const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
313
490
  const ports = new Set;
314
491
  for (const line of result.split(`
315
492
  `)) {
316
- if (line.includes("LISTEN")) {
317
- const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
318
- if (portMatch) {
319
- ports.add(parseInt(portMatch[1]));
320
- }
493
+ const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
494
+ if (portMatch) {
495
+ ports.add(parseInt(portMatch[1]));
321
496
  }
322
497
  }
323
498
  return Array.from(ports);
@@ -333,7 +508,6 @@ var init_platform = __esm(() => {
333
508
 
334
509
  // src/utils.ts
335
510
  import * as fs2 from "fs";
336
- import chalk from "chalk";
337
511
  function parseEnvString(envString) {
338
512
  const env = {};
339
513
  envString.split(",").forEach((pair) => {
@@ -351,8 +525,8 @@ function calculateRuntime(startTime) {
351
525
  }
352
526
  async function getVersion() {
353
527
  try {
354
- const { join } = await import("path");
355
- const pkgPath = join(import.meta.dir, "../package.json");
528
+ const { join: join2 } = await import("path");
529
+ const pkgPath = join2(import.meta.dir, "../package.json");
356
530
  const pkg = await Bun.file(pkgPath).json();
357
531
  return pkg.version || "0.0.0";
358
532
  } catch {
@@ -361,8 +535,7 @@ async function getVersion() {
361
535
  }
362
536
  function validateDirectory(directory) {
363
537
  if (!directory || !fs2.existsSync(directory)) {
364
- console.log(chalk.red("\u274C Error: 'directory' must be a valid path."));
365
- process.exit(1);
538
+ throw new Error(`Directory not found or invalid: '${directory}'`);
366
539
  }
367
540
  }
368
541
  function tailFile(path, prefix, colorFn, lines) {
@@ -417,21 +590,39 @@ var exports_db = {};
417
590
  __export(exports_db, {
418
591
  updateProcessPid: () => updateProcessPid,
419
592
  updateProcessEnv: () => updateProcessEnv,
593
+ saveTemplate: () => saveTemplate,
420
594
  retryDatabaseOperation: () => retryDatabaseOperation,
421
595
  removeProcessByName: () => removeProcessByName,
422
596
  removeProcess: () => removeProcess,
597
+ removeDependency: () => removeDependency,
423
598
  removeAllProcesses: () => removeAllProcesses,
599
+ removeAllDependencies: () => removeAllDependencies,
424
600
  insertProcess: () => insertProcess,
601
+ getTemplate: () => getTemplate,
602
+ getStartOrder: () => getStartOrder,
603
+ getRecentHistory: () => getRecentHistory,
604
+ getProcessHistory: () => getProcessHistory,
425
605
  getProcess: () => getProcess,
606
+ getDependents: () => getDependents,
607
+ getDependencyGraph: () => getDependencyGraph,
608
+ getDependencies: () => getDependencies,
426
609
  getDbInfo: () => getDbInfo,
610
+ getAllTemplates: () => getAllTemplates,
427
611
  getAllProcesses: () => getAllProcesses,
612
+ deleteTemplate: () => deleteTemplate,
428
613
  dbPath: () => dbPath,
429
614
  db: () => db,
615
+ clearOldHistory: () => clearOldHistory,
430
616
  bgrHome: () => bgrHome,
431
- ProcessSchema: () => ProcessSchema
617
+ addHistoryEntry: () => addHistoryEntry,
618
+ addDependency: () => addDependency,
619
+ TemplateSchema: () => TemplateSchema,
620
+ ProcessSchema: () => ProcessSchema,
621
+ HistorySchema: () => HistorySchema,
622
+ DependencySchema: () => DependencySchema
432
623
  });
433
624
  import { Database, z } from "sqlite-zod-orm";
434
- import { join } from "path";
625
+ import { join as join2 } from "path";
435
626
  var {sleep } = globalThis.Bun;
436
627
  import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
437
628
  function getProcess(name) {
@@ -476,6 +667,154 @@ function updateProcessEnv(name, envJson) {
476
667
  db.process.update(proc.id, { env: envJson });
477
668
  }
478
669
  }
670
+ function getAllTemplates() {
671
+ return db.template.select().all();
672
+ }
673
+ function getTemplate(name) {
674
+ return db.template.select().where({ name }).limit(1).get() || null;
675
+ }
676
+ function saveTemplate(data) {
677
+ const existing = db.template.select().where({ name: data.name }).limit(1).get();
678
+ if (existing) {
679
+ db.template.update(existing.id, {
680
+ command: data.command,
681
+ workdir: data.workdir || "",
682
+ env: data.env || "",
683
+ group: data.group || ""
684
+ });
685
+ } else {
686
+ db.template.insert({
687
+ name: data.name,
688
+ command: data.command,
689
+ workdir: data.workdir || "",
690
+ env: data.env || "",
691
+ group: data.group || ""
692
+ });
693
+ }
694
+ }
695
+ function deleteTemplate(name) {
696
+ const tmpl = db.template.select().where({ name }).limit(1).get();
697
+ if (tmpl) {
698
+ db.template.delete(tmpl.id);
699
+ }
700
+ }
701
+ function getProcessHistory(name, limit = 50) {
702
+ return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
703
+ }
704
+ function addHistoryEntry(processName, event, pid, metadata = {}) {
705
+ return db.history.insert({
706
+ process_name: processName,
707
+ event,
708
+ pid,
709
+ metadata: JSON.stringify(metadata)
710
+ });
711
+ }
712
+ function getRecentHistory(limit = 100) {
713
+ return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
714
+ }
715
+ function clearOldHistory(daysToKeep = 30) {
716
+ const cutoff = new Date;
717
+ cutoff.setDate(cutoff.getDate() - daysToKeep);
718
+ const cutoffStr = cutoff.toISOString();
719
+ const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
720
+ for (const entry of oldEntries) {
721
+ db.history.delete(entry.id);
722
+ }
723
+ return oldEntries.length;
724
+ }
725
+ function getDependencies(processName) {
726
+ return db.dependency.select().where({ process_name: processName }).all().map((d) => d.depends_on);
727
+ }
728
+ function getDependents(processName) {
729
+ return db.dependency.select().where({ depends_on: processName }).all().map((d) => d.process_name);
730
+ }
731
+ function getDependencyGraph() {
732
+ const all = db.dependency.select().all();
733
+ const graph = {};
734
+ for (const dep of all) {
735
+ if (!graph[dep.process_name])
736
+ graph[dep.process_name] = [];
737
+ graph[dep.process_name].push(dep.depends_on);
738
+ }
739
+ return graph;
740
+ }
741
+ function addDependency(processName, dependsOn) {
742
+ if (processName === dependsOn)
743
+ return false;
744
+ const existing = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).limit(1).get();
745
+ if (existing)
746
+ return false;
747
+ if (wouldCreateCycle(processName, dependsOn))
748
+ return false;
749
+ db.dependency.insert({ process_name: processName, depends_on: dependsOn });
750
+ return true;
751
+ }
752
+ function removeDependency(processName, dependsOn) {
753
+ const matches = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).all();
754
+ for (const dep of matches) {
755
+ db.dependency.delete(dep.id);
756
+ }
757
+ }
758
+ function removeAllDependencies(processName) {
759
+ const matches = db.dependency.select().where({ process_name: processName }).all();
760
+ for (const dep of matches) {
761
+ db.dependency.delete(dep.id);
762
+ }
763
+ }
764
+ function wouldCreateCycle(processName, dependsOn) {
765
+ const graph = getDependencyGraph();
766
+ if (!graph[processName])
767
+ graph[processName] = [];
768
+ graph[processName].push(dependsOn);
769
+ const visited = new Set;
770
+ const stack = [dependsOn];
771
+ while (stack.length > 0) {
772
+ const current = stack.pop();
773
+ if (current === processName)
774
+ return true;
775
+ if (visited.has(current))
776
+ continue;
777
+ visited.add(current);
778
+ for (const dep of graph[current] || []) {
779
+ stack.push(dep);
780
+ }
781
+ }
782
+ return false;
783
+ }
784
+ function getStartOrder() {
785
+ const graph = getDependencyGraph();
786
+ const allProcesses = getAllProcesses().map((p) => p.name);
787
+ const allNames = new Set(allProcesses);
788
+ const inDegree = {};
789
+ for (const name of allNames)
790
+ inDegree[name] = 0;
791
+ for (const [proc, deps] of Object.entries(graph)) {
792
+ for (const dep of deps) {
793
+ if (allNames.has(dep)) {
794
+ inDegree[proc] = (inDegree[proc] || 0) + 1;
795
+ }
796
+ }
797
+ }
798
+ const queue = [];
799
+ for (const name of allNames) {
800
+ if ((inDegree[name] || 0) === 0)
801
+ queue.push(name);
802
+ }
803
+ const order = [];
804
+ while (queue.length > 0) {
805
+ queue.sort();
806
+ const current = queue.shift();
807
+ order.push(current);
808
+ for (const [proc, deps] of Object.entries(graph)) {
809
+ if (deps.includes(current) && allNames.has(proc)) {
810
+ inDegree[proc]--;
811
+ if (inDegree[proc] === 0)
812
+ queue.push(proc);
813
+ }
814
+ }
815
+ }
816
+ return order;
817
+ }
479
818
  function getDbInfo() {
480
819
  return {
481
820
  dbPath,
@@ -498,7 +837,7 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
498
837
  }
499
838
  throw new Error("Max retries reached for database operation");
500
839
  }
501
- var ProcessSchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
840
+ var ProcessSchema, TemplateSchema, HistorySchema, DependencySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
502
841
  var init_db = __esm(() => {
503
842
  init_platform();
504
843
  ProcessSchema = z.object({
@@ -510,15 +849,36 @@ var init_db = __esm(() => {
510
849
  configPath: z.string().default(""),
511
850
  stdout_path: z.string(),
512
851
  stderr_path: z.string(),
513
- timestamp: z.string().default(() => new Date().toISOString())
852
+ timestamp: z.string().default(() => new Date().toISOString()),
853
+ group: z.string().default("")
854
+ });
855
+ TemplateSchema = z.object({
856
+ name: z.string(),
857
+ command: z.string(),
858
+ workdir: z.string().default(""),
859
+ env: z.string().default(""),
860
+ group: z.string().default(""),
861
+ created_at: z.string().default(() => new Date().toISOString())
862
+ });
863
+ HistorySchema = z.object({
864
+ process_name: z.string(),
865
+ event: z.string(),
866
+ pid: z.number().optional(),
867
+ timestamp: z.string().default(() => new Date().toISOString()),
868
+ metadata: z.string().default("")
869
+ });
870
+ DependencySchema = z.object({
871
+ process_name: z.string(),
872
+ depends_on: z.string(),
873
+ created_at: z.string().default(() => new Date().toISOString())
514
874
  });
515
875
  homePath = getHomeDir();
516
- bgrDir = join(homePath, ".bgr");
876
+ bgrDir = join2(homePath, ".bgr");
517
877
  ensureDir(bgrDir);
518
878
  dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
519
- dbPath = join(bgrDir, dbFilename);
879
+ dbPath = join2(bgrDir, dbFilename);
520
880
  bgrHome = bgrDir;
521
- legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
881
+ legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
522
882
  if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
523
883
  try {
524
884
  copyFileSync2(legacyDbPath, dbPath);
@@ -526,17 +886,23 @@ var init_db = __esm(() => {
526
886
  } catch (e) {}
527
887
  }
528
888
  db = new Database(dbPath, {
529
- process: ProcessSchema
889
+ process: ProcessSchema,
890
+ template: TemplateSchema,
891
+ history: HistorySchema,
892
+ dependency: DependencySchema
530
893
  }, {
531
894
  indexes: {
532
- process: ["name", "timestamp", "pid"]
895
+ process: ["name", "timestamp", "pid"],
896
+ template: ["name"],
897
+ history: ["process_name", "timestamp"],
898
+ dependency: ["process_name", "depends_on"]
533
899
  }
534
900
  });
535
901
  });
536
902
 
537
903
  // src/logger.ts
538
904
  import boxen from "boxen";
539
- import chalk2 from "chalk";
905
+ import chalk from "chalk";
540
906
  function announce(message, title) {
541
907
  console.log(boxen(message, {
542
908
  padding: 1,
@@ -549,7 +915,7 @@ function announce(message, title) {
549
915
  }
550
916
  function error(message) {
551
917
  const text = message instanceof Error ? message.stack || message.message : String(message);
552
- console.error(boxen(chalk2.red(text), {
918
+ console.error(boxen(chalk.red(text), {
553
919
  padding: 1,
554
920
  margin: 1,
555
921
  borderColor: "red",
@@ -557,9 +923,17 @@ function error(message) {
557
923
  titleAlignment: "center",
558
924
  borderStyle: "double"
559
925
  }));
560
- process.exit(1);
926
+ throw new BgrunError(text);
561
927
  }
562
- var init_logger = () => {};
928
+ var BgrunError;
929
+ var init_logger = __esm(() => {
930
+ BgrunError = class BgrunError extends Error {
931
+ constructor(message) {
932
+ super(message);
933
+ this.name = "BgrunError";
934
+ }
935
+ };
936
+ });
563
937
 
564
938
  // src/config.ts
565
939
  function formatEnvKey(key) {
@@ -596,10 +970,10 @@ async function parseConfigFile(configPath) {
596
970
  var exports_deps = {};
597
971
  __export(exports_deps, {
598
972
  getUnmetDeps: () => getUnmetDeps,
599
- getDependencies: () => getDependencies,
973
+ getDependencies: () => getDependencies2,
600
974
  buildDepGraph: () => buildDepGraph
601
975
  });
602
- function getDependencies(envStr) {
976
+ function getDependencies2(envStr) {
603
977
  const env = parseEnvString(envStr);
604
978
  const raw = env.BGR_DEPENDS_ON || "";
605
979
  return raw.split(",").map((s) => s.trim()).filter(Boolean);
@@ -608,7 +982,7 @@ async function buildDepGraph() {
608
982
  const processes = getAllProcesses();
609
983
  const nodeMap = new Map;
610
984
  for (const proc of processes) {
611
- const deps = getDependencies(proc.env);
985
+ const deps = getDependencies2(proc.env);
612
986
  const alive = await isProcessRunning(proc.pid, proc.command);
613
987
  nodeMap.set(proc.name, {
614
988
  name: proc.name,
@@ -660,7 +1034,7 @@ async function getUnmetDeps(name) {
660
1034
  const proc = getProcess(name);
661
1035
  if (!proc)
662
1036
  return [];
663
- const deps = getDependencies(proc.env);
1037
+ const deps = getDependencies2(proc.env);
664
1038
  const unmet = [];
665
1039
  for (const depName of deps) {
666
1040
  const depProc = getProcess(depName);
@@ -684,10 +1058,10 @@ var init_deps = __esm(() => {
684
1058
  // src/commands/run.ts
685
1059
  var {$: $2 } = globalThis.Bun;
686
1060
  var {sleep: sleep2 } = globalThis.Bun;
687
- import { join as join2 } from "path";
1061
+ import { join as join3 } from "path";
688
1062
  import { createMeasure as createMeasure2 } from "measure-fn";
689
1063
  async function handleRun(options) {
690
- const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
1064
+ const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
691
1065
  const existingProcess = name ? getProcess(name) : null;
692
1066
  if (name && existingProcess) {
693
1067
  const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
@@ -708,7 +1082,7 @@ async function handleRun(options) {
708
1082
  const finalDirectory2 = directory || existingProcess.workdir;
709
1083
  validateDirectory(finalDirectory2);
710
1084
  $2.cwd(finalDirectory2);
711
- if (fetch) {
1085
+ if (fetch2) {
712
1086
  if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
713
1087
  error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
714
1088
  }
@@ -758,9 +1132,15 @@ async function handleRun(options) {
758
1132
  if (cmdToMatch) {
759
1133
  await run.measure("Zombie sweep", async () => {
760
1134
  try {
761
- const result = await $2`powershell -Command "Get-Process -Name bun -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -match '${cmdToMatch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").split(" ")[1] || cmdToMatch}' } | Select-Object -ExpandProperty Id"`.nothrow().text();
1135
+ const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1136
+ const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1137
+ if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1138
+ return;
1139
+ }
1140
+ const currentPid = process.pid;
1141
+ 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);
762
1142
  const zombiePids = result.split(`
763
- `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0);
1143
+ `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
764
1144
  for (const zPid of zombiePids) {
765
1145
  await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
766
1146
  }
@@ -781,6 +1161,9 @@ async function handleRun(options) {
781
1161
  const finalCommand = command || existingProcess.command;
782
1162
  const finalDirectory = directory || existingProcess?.workdir;
783
1163
  let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1164
+ if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1165
+ finalEnv.BGR_KEEP_ALIVE = "true";
1166
+ }
784
1167
  let finalConfigPath;
785
1168
  if (configPath !== undefined) {
786
1169
  finalConfigPath = configPath;
@@ -790,7 +1173,7 @@ async function handleRun(options) {
790
1173
  finalConfigPath = ".config.toml";
791
1174
  }
792
1175
  if (finalConfigPath) {
793
- const fullConfigPath = join2(finalDirectory, finalConfigPath);
1176
+ const fullConfigPath = join3(finalDirectory, finalConfigPath);
794
1177
  if (await Bun.file(fullConfigPath).exists()) {
795
1178
  const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
796
1179
  try {
@@ -808,9 +1191,9 @@ async function handleRun(options) {
808
1191
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
809
1192
  }
810
1193
  }
811
- const stdoutPath = stdout || existingProcess?.stdout_path || join2(homePath2, ".bgr", `${name}-out.txt`);
1194
+ const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
812
1195
  Bun.write(stdoutPath, "");
813
- const stderrPath = stderr || existingProcess?.stderr_path || join2(homePath2, ".bgr", `${name}-err.txt`);
1196
+ const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
814
1197
  Bun.write(stderrPath, "");
815
1198
  const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
816
1199
  const newProcess = Bun.spawn(getShellCommand(finalCommand), {
@@ -854,7 +1237,7 @@ __export(exports_log_rotation, {
854
1237
  rotateLogFile: () => rotateLogFile,
855
1238
  rotateAllLogs: () => rotateAllLogs
856
1239
  });
857
- import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
1240
+ import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
858
1241
  function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
859
1242
  try {
860
1243
  if (!existsSync7(filePath))
@@ -870,7 +1253,7 @@ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAU
870
1253
  const truncated = lines.slice(-keepLines);
871
1254
  const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
872
1255
  `;
873
- writeFileSync(filePath, header + truncated.join(`
1256
+ writeFileSync2(filePath, header + truncated.join(`
874
1257
  `));
875
1258
  return true;
876
1259
  } catch {
@@ -922,22 +1305,87 @@ var init_log_rotation = __esm(() => {
922
1305
  var exports_server = {};
923
1306
  __export(exports_server, {
924
1307
  startServer: () => startServer,
925
- guardRestartCounts: () => guardRestartCounts
1308
+ guardRestartCounts: () => guardRestartCounts,
1309
+ guardEvents: () => guardEvents
926
1310
  });
927
1311
  import path2 from "path";
1312
+ async function cleanupPort(port) {
1313
+ if (process.platform !== "win32")
1314
+ return port;
1315
+ try {
1316
+ const proc = Bun.spawn([
1317
+ "powershell",
1318
+ "-NoProfile",
1319
+ "-Command",
1320
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1321
+ ], { stdout: "pipe", stderr: "pipe" });
1322
+ const text = await new Response(proc.stdout).text();
1323
+ const pid = parseInt(text.trim(), 10);
1324
+ if (!pid || pid === process.pid)
1325
+ return port;
1326
+ const checkProc = Bun.spawn([
1327
+ "powershell",
1328
+ "-NoProfile",
1329
+ "-Command",
1330
+ `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
1331
+ ], { stdout: "pipe", stderr: "pipe" });
1332
+ const checkText = await new Response(checkProc.stdout).text();
1333
+ if (checkText.trim()) {
1334
+ console.log(`[server] Killing PID ${pid} holding port ${port}`);
1335
+ Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
1336
+ await Bun.sleep(1000);
1337
+ return port;
1338
+ } else {
1339
+ const fallback = port + 1;
1340
+ console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
1341
+ return fallback;
1342
+ }
1343
+ } catch {
1344
+ return port;
1345
+ }
1346
+ }
928
1347
  async function startServer() {
929
1348
  const { start } = await import("melina");
930
1349
  const appDir = path2.join(import.meta.dir, "../dashboard/app");
931
- const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
1350
+ const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
1351
+ _originalPort = requestedPort;
1352
+ const resolvedPort = await cleanupPort(requestedPort);
1353
+ _currentPort = resolvedPort;
1354
+ const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
932
1355
  await start({
933
1356
  appDir,
934
1357
  defaultTitle: "bgrun Dashboard - Process Manager",
935
1358
  globalCss: path2.join(appDir, "globals.css"),
936
- ...explicitPort !== undefined && { port: explicitPort }
1359
+ ...needsExplicitPort && { port: resolvedPort }
937
1360
  });
938
1361
  startGuard();
939
1362
  const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
940
1363
  startLogRotation2(() => getAllProcesses());
1364
+ if (resolvedPort !== requestedPort) {
1365
+ startStickyPortChecker();
1366
+ }
1367
+ }
1368
+ function startStickyPortChecker() {
1369
+ const CHECK_INTERVAL_MS = 60000;
1370
+ console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
1371
+ setInterval(async () => {
1372
+ if (_currentPort === _originalPort)
1373
+ return;
1374
+ try {
1375
+ const proc = Bun.spawn([
1376
+ "powershell",
1377
+ "-NoProfile",
1378
+ "-Command",
1379
+ `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1380
+ ], { stdout: "pipe", stderr: "pipe" });
1381
+ const text = await new Response(proc.stdout).text();
1382
+ const pid = parseInt(text.trim(), 10);
1383
+ if (!pid) {
1384
+ console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
1385
+ _currentPort = _originalPort;
1386
+ }
1387
+ } catch {}
1388
+ }, CHECK_INTERVAL_MS);
941
1389
  }
942
1390
  function startGuard() {
943
1391
  console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
@@ -959,6 +1407,7 @@ function startGuard() {
959
1407
  if (now < nextRestart)
960
1408
  continue;
961
1409
  console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1410
+ let success = false;
962
1411
  try {
963
1412
  await handleRun({
964
1413
  action: "run",
@@ -966,9 +1415,16 @@ function startGuard() {
966
1415
  force: true,
967
1416
  remoteName: ""
968
1417
  });
1418
+ success = true;
969
1419
  const prevCount = guardRestartCounts.get(proc.name) || 0;
970
1420
  const newCount = prevCount + 1;
971
1421
  guardRestartCounts.set(proc.name, newCount);
1422
+ try {
1423
+ addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1424
+ } catch {}
1425
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1426
+ if (guardEvents.length > 100)
1427
+ guardEvents.pop();
972
1428
  if (newCount > 5) {
973
1429
  const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
974
1430
  guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
@@ -978,6 +1434,9 @@ function startGuard() {
978
1434
  }
979
1435
  } catch (err) {
980
1436
  console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1437
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1438
+ if (guardEvents.length > 100)
1439
+ guardEvents.pop();
981
1440
  }
982
1441
  } else {
983
1442
  const prevCount = guardRestartCounts.get(proc.name) || 0;
@@ -995,7 +1454,7 @@ function startGuard() {
995
1454
  }
996
1455
  }, GUARD_INTERVAL_MS);
997
1456
  }
998
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
1457
+ var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
999
1458
  var init_server = __esm(() => {
1000
1459
  init_db();
1001
1460
  init_platform();
@@ -1007,8 +1466,11 @@ var init_server = __esm(() => {
1007
1466
  _g.__bgrGuardRestartCounts = new Map;
1008
1467
  if (!_g.__bgrGuardNextRestartTime)
1009
1468
  _g.__bgrGuardNextRestartTime = new Map;
1469
+ if (!_g.__bgrGuardEvents)
1470
+ _g.__bgrGuardEvents = [];
1010
1471
  guardRestartCounts = _g.__bgrGuardRestartCounts;
1011
1472
  guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1473
+ guardEvents = _g.__bgrGuardEvents;
1012
1474
  });
1013
1475
 
1014
1476
  // src/guard.ts
@@ -1016,6 +1478,38 @@ var exports_guard = {};
1016
1478
  __export(exports_guard, {
1017
1479
  startGuardLoop: () => startGuardLoop
1018
1480
  });
1481
+ import { createHmac } from "crypto";
1482
+ async function notifyWebhook(event, name, details) {
1483
+ if (!WEBHOOK_URL)
1484
+ return;
1485
+ try {
1486
+ const payload = JSON.stringify({
1487
+ event,
1488
+ process: name,
1489
+ timestamp: new Date().toISOString(),
1490
+ ...details
1491
+ });
1492
+ const headers = {
1493
+ "Content-Type": "application/json",
1494
+ "User-Agent": "bgrun-guard/1.0"
1495
+ };
1496
+ if (WEBHOOK_SECRET) {
1497
+ const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
1498
+ headers["X-BGR-Signature"] = `sha256=${sig}`;
1499
+ }
1500
+ const controller = new AbortController;
1501
+ const timeout = setTimeout(() => controller.abort(), 5000);
1502
+ await fetch(WEBHOOK_URL, {
1503
+ method: "POST",
1504
+ headers,
1505
+ body: payload,
1506
+ signal: controller.signal
1507
+ });
1508
+ clearTimeout(timeout);
1509
+ } catch (err) {
1510
+ console.error(`[guard] Webhook failed: ${err.message}`);
1511
+ }
1512
+ }
1019
1513
  async function restartProcess(name) {
1020
1514
  try {
1021
1515
  await handleRun({
@@ -1064,6 +1558,7 @@ async function guardCycle() {
1064
1558
  continue;
1065
1559
  }
1066
1560
  console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
1561
+ notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
1067
1562
  const success = await restartProcess(proc.name);
1068
1563
  if (success) {
1069
1564
  const count = (state.restartCounts.get(proc.name) || 0) + 1;
@@ -1077,6 +1572,9 @@ async function guardCycle() {
1077
1572
  console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
1078
1573
  }
1079
1574
  restarted++;
1575
+ notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
1576
+ } else {
1577
+ notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
1080
1578
  }
1081
1579
  } else if (alive) {
1082
1580
  const count = state.restartCounts.get(proc.name) || 0;
@@ -1111,17 +1609,20 @@ async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
1111
1609
  console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
1112
1610
  console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
1113
1611
  console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
1612
+ console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
1114
1613
  console.log(`[guard] Started: ${new Date().toLocaleString()}`);
1115
1614
  console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
1116
1615
  await guardCycle();
1117
1616
  setInterval(guardCycle, interval);
1118
1617
  }
1119
- var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1618
+ var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1120
1619
  var init_guard = __esm(() => {
1121
1620
  init_db();
1122
1621
  init_platform();
1123
1622
  init_run();
1124
1623
  init_utils();
1624
+ WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
1625
+ WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
1125
1626
  MAX_BACKOFF_MS = 5 * 60000;
1126
1627
  state = {
1127
1628
  restartCounts: new Map,
@@ -1136,10 +1637,10 @@ init_run();
1136
1637
  import { parseArgs } from "util";
1137
1638
 
1138
1639
  // src/commands/list.ts
1139
- import chalk4 from "chalk";
1640
+ import chalk3 from "chalk";
1140
1641
 
1141
1642
  // src/table.ts
1142
- import chalk3 from "chalk";
1643
+ import chalk2 from "chalk";
1143
1644
  function getTerminalWidth() {
1144
1645
  return process.stdout.columns || 120;
1145
1646
  }
@@ -1221,7 +1722,7 @@ function renderBorder(widths, padding, style) {
1221
1722
  function renderHorizontalTable(rows, columns, options = {}) {
1222
1723
  const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
1223
1724
  if (rows.length === 0)
1224
- return { table: chalk3.gray("No data to display"), truncatedIndices: [] };
1725
+ return { table: chalk2.gray("No data to display"), truncatedIndices: [] };
1225
1726
  const borderChars = {
1226
1727
  rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
1227
1728
  single: ["\u250C", "\u252C", "\u2510", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2514", "\u2534", "\u2518"],
@@ -1237,7 +1738,7 @@ function renderHorizontalTable(rows, columns, options = {}) {
1237
1738
  if (borderStyle !== "none")
1238
1739
  lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
1239
1740
  if (showHeaders) {
1240
- const headerCells = columns.map((col, i) => chalk3.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
1741
+ const headerCells = columns.map((col, i) => chalk2.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
1241
1742
  lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
1242
1743
  if (borderStyle !== "none")
1243
1744
  lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
@@ -1266,10 +1767,10 @@ function renderVerticalTree(rows, columns) {
1266
1767
  if (index > 0)
1267
1768
  lines.push("");
1268
1769
  const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
1269
- lines.push(chalk3.cyan(`\u25B6 ${name}`));
1770
+ lines.push(chalk2.cyan(`\u25B6 ${name}`));
1270
1771
  columns.forEach((col) => {
1271
1772
  const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
1272
- lines.push(` \u251C\u2500 ${chalk3.gray(`${col.header}:`)} ${value}`);
1773
+ lines.push(` \u251C\u2500 ${chalk2.gray(`${col.header}:`)} ${value}`);
1273
1774
  });
1274
1775
  });
1275
1776
  return lines.join(`
@@ -1288,15 +1789,15 @@ function renderHybridTable(rows, columns, options = {}) {
1288
1789
  }
1289
1790
  function renderProcessTable(processes, options) {
1290
1791
  const columns = [
1291
- { key: "id", header: "ID", formatter: (id) => chalk3.blue(id) },
1292
- { key: "pid", header: "PID", formatter: (pid) => chalk3.yellow(pid) },
1293
- { key: "name", header: "Name", formatter: (name) => chalk3.cyan.bold(name) },
1294
- { key: "port", header: "Port", formatter: (port) => port === "-" ? chalk3.gray(port) : chalk3.hex("#FF6B6B")(port) },
1295
- { key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk3.gray(mem) : chalk3.hex("#4ECDC4")(mem) },
1792
+ { key: "id", header: "ID", formatter: (id) => chalk2.blue(id) },
1793
+ { key: "pid", header: "PID", formatter: (pid) => chalk2.yellow(pid) },
1794
+ { key: "name", header: "Name", formatter: (name) => chalk2.cyan.bold(name) },
1795
+ { key: "port", header: "Port", formatter: (port) => port === "-" ? chalk2.gray(port) : chalk2.hex("#FF6B6B")(port) },
1796
+ { key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk2.gray(mem) : chalk2.hex("#4ECDC4")(mem) },
1296
1797
  { key: "command", header: "Command" },
1297
- { key: "workdir", header: "Directory", formatter: (dir) => chalk3.gray(dir), truncator: truncatePath },
1798
+ { key: "workdir", header: "Directory", formatter: (dir) => chalk2.gray(dir), truncator: truncatePath },
1298
1799
  { key: "status", header: "Status" },
1299
- { key: "runtime", header: "Runtime", formatter: (runtime) => chalk3.magenta(runtime) }
1800
+ { key: "runtime", header: "Runtime", formatter: (runtime) => chalk2.magenta(runtime) }
1300
1801
  ];
1301
1802
  return renderHybridTable(processes, columns, options);
1302
1803
  }
@@ -1322,10 +1823,29 @@ async function showAll(opts) {
1322
1823
  const envVars = parseEnvString(proc.env);
1323
1824
  return envVars["BGR_GROUP"] === opts.filter;
1324
1825
  });
1826
+ const deadPids = new Set;
1827
+ const aliveCache = new Map;
1828
+ for (const proc of filtered) {
1829
+ const alive = await isProcessRunning(proc.pid, proc.command);
1830
+ aliveCache.set(proc.pid, alive);
1831
+ if (!alive && proc.pid > 0)
1832
+ deadPids.add(proc.pid);
1833
+ }
1834
+ if (deadPids.size > 0) {
1835
+ const reconciled = await reconcileProcessPids(filtered.map((p) => ({ name: p.name, pid: p.pid, command: p.command, workdir: p.workdir })), deadPids);
1836
+ for (const [name, newPid] of reconciled) {
1837
+ updateProcessPid(name, newPid);
1838
+ const proc = filtered.find((p) => p.name === name);
1839
+ if (proc) {
1840
+ proc.pid = newPid;
1841
+ aliveCache.set(newPid, true);
1842
+ }
1843
+ }
1844
+ }
1325
1845
  if (opts?.json) {
1326
1846
  const jsonData = [];
1327
1847
  for (const proc of filtered) {
1328
- const isRunning = await isProcessRunning(proc.pid, proc.command);
1848
+ const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1329
1849
  const envVars = parseEnvString(proc.env);
1330
1850
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1331
1851
  jsonData.push({
@@ -1343,7 +1863,7 @@ async function showAll(opts) {
1343
1863
  const allPids = filtered.map((p) => p.pid);
1344
1864
  const resourceMap = await getProcessBatchResources(allPids);
1345
1865
  for (const proc of filtered) {
1346
- const isRunning = await isProcessRunning(proc.pid, proc.command);
1866
+ const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1347
1867
  const runtime = calculateRuntime(proc.timestamp);
1348
1868
  const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1349
1869
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
@@ -1355,7 +1875,7 @@ async function showAll(opts) {
1355
1875
  memory: formatMemory(mem),
1356
1876
  command: proc.command,
1357
1877
  workdir: proc.workdir,
1358
- status: isRunning ? chalk4.green.bold("\u25CF Running") : chalk4.red.bold("\u25CB Stopped"),
1878
+ status: isRunning ? chalk3.green.bold("\u25CF Running") : chalk3.red.bold("\u25CB Stopped"),
1359
1879
  runtime
1360
1880
  });
1361
1881
  }
@@ -1375,7 +1895,7 @@ async function showAll(opts) {
1375
1895
  console.log(tableOutput);
1376
1896
  const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
1377
1897
  const stoppedCount = tableData.filter((p) => p.status.includes("Stopped")).length;
1378
- console.log(chalk4.cyan(`Total: ${tableData.length} processes (${chalk4.green(`${runningCount} running`)}, ${chalk4.red(`${stoppedCount} stopped`)})`));
1898
+ console.log(chalk3.cyan(`Total: ${tableData.length} processes (${chalk3.green(`${runningCount} running`)}, ${chalk3.red(`${stoppedCount} stopped`)})`));
1379
1899
  }
1380
1900
 
1381
1901
  // src/commands/cleanup.ts
@@ -1506,7 +2026,7 @@ init_utils();
1506
2026
  init_run();
1507
2027
  import * as fs4 from "fs";
1508
2028
  import path from "path";
1509
- import chalk5 from "chalk";
2029
+ import chalk4 from "chalk";
1510
2030
  async function handleWatch(options, logOptions) {
1511
2031
  let currentProcess = null;
1512
2032
  let isRestarting = false;
@@ -1517,7 +2037,7 @@ async function handleWatch(options, logOptions) {
1517
2037
  const isDead = !await isProcessRunning(proc.pid);
1518
2038
  if (!isDead)
1519
2039
  return false;
1520
- console.log(chalk5.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
2040
+ console.log(chalk4.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
1521
2041
  const readAndDump = (path2, color, label) => {
1522
2042
  try {
1523
2043
  if (fs4.existsSync(path2)) {
@@ -1531,14 +2051,14 @@ ${color(content)}
1531
2051
  }
1532
2052
  }
1533
2053
  } catch (err) {
1534
- console.warn(chalk5.gray(`Could not read ${label} log: ${err}`));
2054
+ console.warn(chalk4.gray(`Could not read ${label} log: ${err}`));
1535
2055
  }
1536
2056
  };
1537
2057
  if (logOptions.logType === "both" || logOptions.logType === "stdout") {
1538
- readAndDump(proc.stdout_path, chalk5.white, "\uD83D\uDCC4 Stdout");
2058
+ readAndDump(proc.stdout_path, chalk4.white, "\uD83D\uDCC4 Stdout");
1539
2059
  }
1540
2060
  if (logOptions.logType === "both" || logOptions.logType === "stderr") {
1541
- readAndDump(proc.stderr_path, chalk5.red, "\uD83D\uDCC4 Stderr");
2061
+ readAndDump(proc.stderr_path, chalk4.red, "\uD83D\uDCC4 Stderr");
1542
2062
  }
1543
2063
  return true;
1544
2064
  };
@@ -1579,29 +2099,29 @@ ${color(content)}
1579
2099
  const stops = [];
1580
2100
  if (!logOptions.showLogs || !currentProcess)
1581
2101
  return stops;
1582
- console.log(chalk5.gray(`
2102
+ console.log(chalk4.gray(`
1583
2103
  ` + "\u2500".repeat(50) + `
1584
2104
  `));
1585
2105
  if (logOptions.logType === "both" || logOptions.logType === "stdout") {
1586
- console.log(chalk5.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
1587
- console.log(chalk5.gray("\u2550".repeat(50)));
2106
+ console.log(chalk4.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
2107
+ console.log(chalk4.gray("\u2550".repeat(50)));
1588
2108
  try {
1589
2109
  await waitForLogReady(currentProcess.stdout_path);
1590
2110
  } catch (err) {
1591
- console.warn(chalk5.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
2111
+ console.warn(chalk4.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
1592
2112
  }
1593
- const stop = tailFile(currentProcess.stdout_path, "", chalk5.white, logOptions.lines);
2113
+ const stop = tailFile(currentProcess.stdout_path, "", chalk4.white, logOptions.lines);
1594
2114
  stops.push(stop);
1595
2115
  }
1596
2116
  if (logOptions.logType === "both" || logOptions.logType === "stderr") {
1597
- console.log(chalk5.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
1598
- console.log(chalk5.gray("\u2550".repeat(50)));
2117
+ console.log(chalk4.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
2118
+ console.log(chalk4.gray("\u2550".repeat(50)));
1599
2119
  try {
1600
2120
  await waitForLogReady(currentProcess.stderr_path);
1601
2121
  } catch (err) {
1602
- console.warn(chalk5.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
2122
+ console.warn(chalk4.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
1603
2123
  }
1604
- const stop = tailFile(currentProcess.stderr_path, "", chalk5.red, logOptions.lines);
2124
+ const stop = tailFile(currentProcess.stderr_path, "", chalk4.red, logOptions.lines);
1605
2125
  stops.push(stop);
1606
2126
  }
1607
2127
  return stops;
@@ -1626,7 +2146,7 @@ ${color(content)}
1626
2146
  const died = await dumpLogsIfDead(currentProcess, restartReason);
1627
2147
  if (died) {
1628
2148
  if (lastRestartPath) {
1629
- console.log(chalk5.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
2149
+ console.log(chalk4.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
1630
2150
  return;
1631
2151
  } else {
1632
2152
  error(`Failed to start process '${options.name}'. Aborting watch mode.`);
@@ -1639,7 +2159,7 @@ ${color(content)}
1639
2159
  } finally {
1640
2160
  isRestarting = false;
1641
2161
  if (currentProcess) {
1642
- console.log(chalk5.cyan(`
2162
+ console.log(chalk4.cyan(`
1643
2163
  \uD83D\uDC40 Watching for file changes in: ${currentProcess.workdir}`));
1644
2164
  }
1645
2165
  }
@@ -1659,7 +2179,7 @@ ${color(content)}
1659
2179
  }
1660
2180
  tailStops = await startTails();
1661
2181
  const workdir = currentProcess.workdir;
1662
- console.log(chalk5.cyan(`
2182
+ console.log(chalk4.cyan(`
1663
2183
  \uD83D\uDC40 Watching for file changes in: ${workdir}`));
1664
2184
  const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
1665
2185
  if (filename == null)
@@ -1672,7 +2192,7 @@ ${color(content)}
1672
2192
  debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
1673
2193
  });
1674
2194
  const cleanup = async () => {
1675
- console.log(chalk5.magenta(`
2195
+ console.log(chalk4.magenta(`
1676
2196
  SIGINT received...`));
1677
2197
  watcher.close();
1678
2198
  tailStops.forEach((stop) => stop());
@@ -1695,7 +2215,7 @@ SIGINT received...`));
1695
2215
  init_db();
1696
2216
  init_logger();
1697
2217
  init_platform();
1698
- import chalk6 from "chalk";
2218
+ import chalk5 from "chalk";
1699
2219
  import * as fs5 from "fs";
1700
2220
  async function showLogs(name, logType = "both", lines) {
1701
2221
  const proc = getProcess(name);
@@ -1704,17 +2224,17 @@ async function showLogs(name, logType = "both", lines) {
1704
2224
  return;
1705
2225
  }
1706
2226
  if (logType === "both" || logType === "stdout") {
1707
- console.log(chalk6.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
1708
- console.log(chalk6.gray("\u2550".repeat(50)));
2227
+ console.log(chalk5.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
2228
+ console.log(chalk5.gray("\u2550".repeat(50)));
1709
2229
  if (fs5.existsSync(proc.stdout_path)) {
1710
2230
  try {
1711
2231
  const output = await readFileTail(proc.stdout_path, lines);
1712
- console.log(output || chalk6.gray("(no output)"));
2232
+ console.log(output || chalk5.gray("(no output)"));
1713
2233
  } catch (err) {
1714
- console.log(chalk6.red(`Error reading stdout: ${err}`));
2234
+ console.log(chalk5.red(`Error reading stdout: ${err}`));
1715
2235
  }
1716
2236
  } else {
1717
- console.log(chalk6.gray("(log file not found)"));
2237
+ console.log(chalk5.gray("(log file not found)"));
1718
2238
  }
1719
2239
  if (logType === "both") {
1720
2240
  console.log(`
@@ -1722,17 +2242,17 @@ async function showLogs(name, logType = "both", lines) {
1722
2242
  }
1723
2243
  }
1724
2244
  if (logType === "both" || logType === "stderr") {
1725
- console.log(chalk6.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
1726
- console.log(chalk6.gray("\u2550".repeat(50)));
2245
+ console.log(chalk5.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
2246
+ console.log(chalk5.gray("\u2550".repeat(50)));
1727
2247
  if (fs5.existsSync(proc.stderr_path)) {
1728
2248
  try {
1729
2249
  const output = await readFileTail(proc.stderr_path, lines);
1730
- console.log(output || chalk6.gray("(no errors)"));
2250
+ console.log(output || chalk5.gray("(no errors)"));
1731
2251
  } catch (err) {
1732
- console.log(chalk6.red(`Error reading stderr: ${err}`));
2252
+ console.log(chalk5.red(`Error reading stderr: ${err}`));
1733
2253
  }
1734
2254
  } else {
1735
- console.log(chalk6.gray("(log file not found)"));
2255
+ console.log(chalk5.gray("(log file not found)"));
1736
2256
  }
1737
2257
  }
1738
2258
  }
@@ -1742,35 +2262,44 @@ init_logger();
1742
2262
  init_db();
1743
2263
  init_utils();
1744
2264
  init_platform();
1745
- import chalk7 from "chalk";
2265
+ import chalk6 from "chalk";
1746
2266
  async function showDetails(name) {
1747
2267
  const proc = getProcess(name);
1748
2268
  if (!proc) {
1749
2269
  error(`No process found named '${name}'`);
1750
2270
  return;
1751
2271
  }
1752
- const isRunning = await isProcessRunning(proc.pid, proc.command);
2272
+ let isRunning = await isProcessRunning(proc.pid, proc.command);
2273
+ if (!isRunning && proc.pid > 0) {
2274
+ const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
2275
+ const newPid = reconciled.get(proc.name);
2276
+ if (newPid) {
2277
+ updateProcessPid(proc.name, newPid);
2278
+ proc.pid = newPid;
2279
+ isRunning = true;
2280
+ }
2281
+ }
1753
2282
  const runtime = calculateRuntime(proc.timestamp);
1754
2283
  const envVars = parseEnvString(proc.env);
1755
2284
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1756
- const portDisplay = ports.length > 0 ? ports.map((p) => chalk7.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
2285
+ const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
1757
2286
  const details = `
1758
- ${chalk7.bold("Process Details:")}
1759
- ${chalk7.gray("\u2550".repeat(50))}
1760
- ${chalk7.cyan.bold("Name:")} ${proc.name}
1761
- ${chalk7.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
1762
- ${chalk7.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
1763
- ${chalk7.bold("Status:")} ${isRunning ? chalk7.green.bold("\u25CF Running") : chalk7.red.bold("\u25CB Stopped")}
1764
- ${chalk7.magenta.bold("Runtime:")} ${runtime}
1765
- ${chalk7.blue.bold("Working Directory:")} ${proc.workdir}
1766
- ${chalk7.white.bold("Command:")} ${proc.command}
1767
- ${chalk7.gray.bold("Config Path:")} ${proc.configPath}
1768
- ${chalk7.green.bold("Stdout Path:")} ${proc.stdout_path}
1769
- ${chalk7.red.bold("Stderr Path:")} ${proc.stderr_path}
2287
+ ${chalk6.bold("Process Details:")}
2288
+ ${chalk6.gray("\u2550".repeat(50))}
2289
+ ${chalk6.cyan.bold("Name:")} ${proc.name}
2290
+ ${chalk6.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
2291
+ ${chalk6.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
2292
+ ${chalk6.bold("Status:")} ${isRunning ? chalk6.green.bold("\u25CF Running") : chalk6.red.bold("\u25CB Stopped")}
2293
+ ${chalk6.magenta.bold("Runtime:")} ${runtime}
2294
+ ${chalk6.blue.bold("Working Directory:")} ${proc.workdir}
2295
+ ${chalk6.white.bold("Command:")} ${proc.command}
2296
+ ${chalk6.gray.bold("Config Path:")} ${proc.configPath}
2297
+ ${chalk6.green.bold("Stdout Path:")} ${proc.stdout_path}
2298
+ ${chalk6.red.bold("Stderr Path:")} ${proc.stderr_path}
1770
2299
 
1771
- ${chalk7.bold("\uD83D\uDD27 Environment Variables:")}
1772
- ${chalk7.gray("\u2550".repeat(50))}
1773
- ${Object.entries(envVars).map(([key, value]) => `${chalk7.cyan.bold(key)} = ${chalk7.yellow(value)}`).join(`
2300
+ ${chalk6.bold("\uD83D\uDD27 Environment Variables:")}
2301
+ ${chalk6.gray("\u2550".repeat(50))}
2302
+ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${chalk6.yellow(value)}`).join(`
1774
2303
  `)}
1775
2304
  `;
1776
2305
  announce(details, `Process Details: ${name}`);
@@ -1781,8 +2310,8 @@ init_logger();
1781
2310
  init_platform();
1782
2311
  init_db();
1783
2312
  import dedent from "dedent";
1784
- import chalk8 from "chalk";
1785
- import { join as join3 } from "path";
2313
+ import chalk7 from "chalk";
2314
+ import { join as join4 } from "path";
1786
2315
  var {sleep: sleep3 } = globalThis.Bun;
1787
2316
  import { configure } from "measure-fn";
1788
2317
  if (!Bun.argv.includes("--_serve")) {
@@ -1790,15 +2319,55 @@ if (!Bun.argv.includes("--_serve")) {
1790
2319
  configure({ silent: true });
1791
2320
  }
1792
2321
  }
2322
+ function redirectConsoleToFiles() {
2323
+ const stdoutPath = Bun.env.BGR_STDOUT;
2324
+ const stderrPath = Bun.env.BGR_STDERR;
2325
+ if (!stdoutPath && !stderrPath)
2326
+ return;
2327
+ const { appendFileSync } = __require("fs");
2328
+ const stripAnsi2 = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
2329
+ const timestamp = () => new Date().toISOString().replace("T", " ").substring(0, 19);
2330
+ if (stdoutPath) {
2331
+ const origLog = console.log;
2332
+ const origWarn = console.warn;
2333
+ console.log = (...args) => {
2334
+ const line = `[${timestamp()}] ${stripAnsi2(args.map(String).join(" "))}
2335
+ `;
2336
+ try {
2337
+ appendFileSync(stdoutPath, line);
2338
+ } catch {}
2339
+ origLog.apply(console, args);
2340
+ };
2341
+ console.warn = (...args) => {
2342
+ const line = `[${timestamp()}] WARN: ${stripAnsi2(args.map(String).join(" "))}
2343
+ `;
2344
+ try {
2345
+ appendFileSync(stdoutPath, line);
2346
+ } catch {}
2347
+ origWarn.apply(console, args);
2348
+ };
2349
+ }
2350
+ if (stderrPath) {
2351
+ const origError = console.error;
2352
+ console.error = (...args) => {
2353
+ const line = `[${timestamp()}] ERROR: ${stripAnsi2(args.map(String).join(" "))}
2354
+ `;
2355
+ try {
2356
+ appendFileSync(stderrPath, line);
2357
+ } catch {}
2358
+ origError.apply(console, args);
2359
+ };
2360
+ }
2361
+ }
1793
2362
  async function showHelp() {
1794
2363
  const usage = dedent`
1795
- ${chalk8.bold("bgrun \u2014 Bun Background Runner")}
1796
- ${chalk8.gray("\u2550".repeat(50))}
2364
+ ${chalk7.bold("bgrun \u2014 Bun Background Runner")}
2365
+ ${chalk7.gray("\u2550".repeat(50))}
1797
2366
 
1798
- ${chalk8.yellow("Usage:")}
2367
+ ${chalk7.yellow("Usage:")}
1799
2368
  bgrun [name] [options]
1800
2369
 
1801
- ${chalk8.yellow("Commands:")}
2370
+ ${chalk7.yellow("Commands:")}
1802
2371
  bgrun List all processes
1803
2372
  bgrun [name] Show details for a process
1804
2373
  bgrun --dashboard Launch web dashboard (managed by bgrun)
@@ -1811,7 +2380,7 @@ async function showHelp() {
1811
2380
  bgrun --clean Remove all stopped processes
1812
2381
  bgrun --nuke Delete ALL processes
1813
2382
 
1814
- ${chalk8.yellow("Options:")}
2383
+ ${chalk7.yellow("Options:")}
1815
2384
  --name <string> Process name (required for new)
1816
2385
  --command <string> Process command (required for new)
1817
2386
  --directory <path> Working directory (required for new)
@@ -1831,7 +2400,7 @@ async function showHelp() {
1831
2400
  --port <number> Port for dashboard (default: 3000)
1832
2401
  --help Show this help message
1833
2402
 
1834
- ${chalk8.yellow("Examples:")}
2403
+ ${chalk7.yellow("Examples:")}
1835
2404
  bgrun --dashboard
1836
2405
  bgrun --name myapp --command "bun run dev" --directory . --watch
1837
2406
  bgrun myapp --logs --lines 50
@@ -1878,11 +2447,13 @@ async function run2() {
1878
2447
  allowPositionals: true
1879
2448
  });
1880
2449
  if (values["_serve"]) {
2450
+ redirectConsoleToFiles();
1881
2451
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
1882
2452
  await startServer2();
1883
2453
  return;
1884
2454
  }
1885
2455
  if (values["_guard-loop"]) {
2456
+ redirectConsoleToFiles();
1886
2457
  const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
1887
2458
  const intervalStr = positionals[0];
1888
2459
  const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
@@ -1892,7 +2463,7 @@ async function run2() {
1892
2463
  if (values.dashboard) {
1893
2464
  const dashboardName = "bgr-dashboard";
1894
2465
  const homePath3 = getHomeDir();
1895
- const bgrDir2 = join3(homePath3, ".bgr");
2466
+ const bgrDir2 = join4(homePath3, ".bgr");
1896
2467
  const requestedPort = values.port;
1897
2468
  const existing = getProcess(dashboardName);
1898
2469
  if (existing && await isProcessRunning(existing.pid)) {
@@ -1906,10 +2477,10 @@ async function run2() {
1906
2477
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
1907
2478
  announce(`Dashboard is already running (PID ${existing.pid})
1908
2479
 
1909
- ` + ` \uD83C\uDF10 ${chalk8.cyan(`http://localhost${portStr}`)}
2480
+ ` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
1910
2481
 
1911
- Use ${chalk8.yellow(`bgrun --stop ${dashboardName}`)} to stop it
1912
- Use ${chalk8.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
2482
+ Use ${chalk7.yellow(`bgrun --stop ${dashboardName}`)} to stop it
2483
+ Use ${chalk7.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
1913
2484
  return;
1914
2485
  }
1915
2486
  if (existing) {
@@ -1927,35 +2498,39 @@ async function run2() {
1927
2498
  const scriptPath = resolve(process.argv[1]);
1928
2499
  const spawnCommand = `bun run ${scriptPath} --_serve`;
1929
2500
  const command = `bgrun --_serve`;
1930
- const stdoutPath = join3(bgrDir2, `${dashboardName}-out.txt`);
1931
- const stderrPath = join3(bgrDir2, `${dashboardName}-err.txt`);
2501
+ const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
2502
+ const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
1932
2503
  await Bun.write(stdoutPath, "");
1933
2504
  await Bun.write(stderrPath, "");
1934
2505
  const spawnEnv = { ...Bun.env };
1935
2506
  if (requestedPort) {
1936
2507
  spawnEnv.BUN_PORT = requestedPort;
1937
2508
  }
2509
+ spawnEnv.BGR_STDOUT = stdoutPath;
2510
+ spawnEnv.BGR_STDERR = stderrPath;
1938
2511
  const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
1939
2512
  if (!isNaN(targetPort) && targetPort > 0) {
1940
2513
  const portFree = await isPortFree(targetPort);
1941
2514
  if (!portFree) {
1942
- console.log(chalk8.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
2515
+ console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
1943
2516
  await killProcessOnPort(targetPort);
1944
2517
  const freed = await waitForPortFree(targetPort, 5000);
1945
2518
  if (!freed) {
1946
- console.log(chalk8.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
2519
+ console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
1947
2520
  }
1948
2521
  }
1949
2522
  }
1950
2523
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1951
2524
  env: spawnEnv,
1952
2525
  cwd: bgrDir2,
1953
- stdout: Bun.file(stdoutPath),
1954
- stderr: Bun.file(stderrPath)
2526
+ stdout: "ignore",
2527
+ stderr: "ignore",
2528
+ detached: true
1955
2529
  });
1956
2530
  newProcess.unref();
1957
2531
  await sleep3(2000);
1958
- const actualPid = await findChildPid(newProcess.pid);
2532
+ const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2533
+ const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
1959
2534
  let actualPort = null;
1960
2535
  for (let attempt = 0;attempt < 10; attempt++) {
1961
2536
  const ports = await getProcessPorts(actualPid);
@@ -1978,19 +2553,19 @@ async function run2() {
1978
2553
  const portDisplay = actualPort ? String(actualPort) : "(detecting...)";
1979
2554
  const urlDisplay = actualPort ? `http://localhost:${actualPort}` : "http://localhost (port auto-assigned)";
1980
2555
  const msg = dedent`
1981
- ${chalk8.bold("\u26A1 BGR Dashboard launched")}
1982
- ${chalk8.gray("\u2500".repeat(40))}
2556
+ ${chalk7.bold("\u26A1 BGR Dashboard launched")}
2557
+ ${chalk7.gray("\u2500".repeat(40))}
1983
2558
 
1984
- \u{1f310} Open in browser: ${chalk8.cyan.underline(urlDisplay)}
2559
+ \u{1f310} Open in browser: ${chalk7.cyan.underline(urlDisplay)}
1985
2560
  \u{1f4ca} Manage all your processes from the web UI
1986
2561
  \u{1f504} Auto-refreshes every 3 seconds
1987
2562
 
1988
- ${chalk8.gray("\u2500".repeat(40))}
1989
- Process: ${chalk8.white(dashboardName)} | PID: ${chalk8.white(String(actualPid))} | Port: ${chalk8.white(portDisplay)}
2563
+ ${chalk7.gray("\u2500".repeat(40))}
2564
+ Process: ${chalk7.white(dashboardName)} | PID: ${chalk7.white(String(actualPid))} | Port: ${chalk7.white(portDisplay)}
1990
2565
 
1991
- ${chalk8.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
1992
- ${chalk8.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
1993
- ${chalk8.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
2566
+ ${chalk7.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
2567
+ ${chalk7.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
2568
+ ${chalk7.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
1994
2569
  `;
1995
2570
  announce(msg, "BGR Dashboard");
1996
2571
  return;
@@ -1998,13 +2573,13 @@ async function run2() {
1998
2573
  if (values.guard) {
1999
2574
  const guardName = "bgr-guard";
2000
2575
  const homePath3 = getHomeDir();
2001
- const bgrDir2 = join3(homePath3, ".bgr");
2576
+ const bgrDir2 = join4(homePath3, ".bgr");
2002
2577
  const existing = getProcess(guardName);
2003
2578
  if (existing && await isProcessRunning(existing.pid)) {
2004
2579
  announce(`Guard is already running (PID ${existing.pid})
2005
2580
 
2006
- Use ${chalk8.yellow(`bgrun --stop ${guardName}`)} to stop it
2007
- Use ${chalk8.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2581
+ Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
2582
+ Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2008
2583
  return;
2009
2584
  }
2010
2585
  if (existing) {
@@ -2017,19 +2592,27 @@ async function run2() {
2017
2592
  const scriptPath = resolve(process.argv[1]);
2018
2593
  const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
2019
2594
  const command = `bgrun --_guard-loop`;
2020
- const stdoutPath = join3(bgrDir2, `${guardName}-out.txt`);
2021
- const stderrPath = join3(bgrDir2, `${guardName}-err.txt`);
2595
+ const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
2596
+ const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
2022
2597
  await Bun.write(stdoutPath, "");
2023
2598
  await Bun.write(stderrPath, "");
2024
2599
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
2025
- env: { ...Bun.env },
2600
+ env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
2026
2601
  cwd: bgrDir2,
2027
- stdout: Bun.file(stdoutPath),
2028
- stderr: Bun.file(stderrPath)
2602
+ stdout: "ignore",
2603
+ stderr: "ignore",
2604
+ detached: true
2029
2605
  });
2030
2606
  newProcess.unref();
2031
2607
  await sleep3(1000);
2032
- const actualPid = await findChildPid(newProcess.pid);
2608
+ let actualPid = await findChildPid(newProcess.pid);
2609
+ if (!await isProcessRunning(actualPid)) {
2610
+ const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
2611
+ const result = ps(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '_guard-loop' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 3000);
2612
+ const foundPid = parseInt(result.trim());
2613
+ if (!isNaN(foundPid) && foundPid > 0)
2614
+ actualPid = foundPid;
2615
+ }
2033
2616
  await retryDatabaseOperation(() => insertProcess({
2034
2617
  pid: actualPid,
2035
2618
  workdir: bgrDir2,
@@ -2041,19 +2624,19 @@ async function run2() {
2041
2624
  stderr_path: stderrPath
2042
2625
  }));
2043
2626
  const msg = dedent`
2044
- ${chalk8.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2045
- ${chalk8.gray("\u2500".repeat(40))}
2627
+ ${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2628
+ ${chalk7.gray("\u2500".repeat(40))}
2046
2629
 
2047
2630
  Monitors: All processes with BGR_KEEP_ALIVE=true
2048
2631
  Also watches: bgr-dashboard (auto-restart if it dies)
2049
2632
  Check interval: 30 seconds
2050
2633
  Backoff: Exponential after 5 rapid crashes
2051
2634
 
2052
- ${chalk8.gray("\u2500".repeat(40))}
2053
- Process: ${chalk8.white(guardName)} | PID: ${chalk8.white(String(actualPid))}
2635
+ ${chalk7.gray("\u2500".repeat(40))}
2636
+ Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
2054
2637
 
2055
- ${chalk8.yellow(`bgrun ${guardName} --logs`)} View guard logs
2056
- ${chalk8.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2638
+ ${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
2639
+ ${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2057
2640
  `;
2058
2641
  announce(msg, "BGR Guard");
2059
2642
  return;
@@ -2070,13 +2653,13 @@ async function run2() {
2070
2653
  const info = getDbInfo();
2071
2654
  const version = await getVersion();
2072
2655
  console.log(dedent`
2073
- ${chalk8.bold("bgrun debug info")}
2074
- ${chalk8.gray("\u2500".repeat(40))}
2075
- Version: ${chalk8.cyan(version)}
2076
- BGR Home: ${chalk8.yellow(info.bgrHome)}
2077
- DB Path: ${chalk8.yellow(info.dbPath)}
2656
+ ${chalk7.bold("bgrun debug info")}
2657
+ ${chalk7.gray("\u2500".repeat(40))}
2658
+ Version: ${chalk7.cyan(version)}
2659
+ BGR Home: ${chalk7.yellow(info.bgrHome)}
2660
+ DB Path: ${chalk7.yellow(info.dbPath)}
2078
2661
  DB File: ${info.dbFilename}
2079
- DB Exists: ${info.exists ? chalk8.green("\u2713") : chalk8.red("\u2717")}
2662
+ DB Exists: ${info.exists ? chalk7.green("\u2713") : chalk7.red("\u2717")}
2080
2663
  Platform: ${process.platform}
2081
2664
  Bun: ${Bun.version}
2082
2665
  `);
@@ -2097,12 +2680,12 @@ async function run2() {
2097
2680
  error("No processes registered.");
2098
2681
  return;
2099
2682
  }
2100
- console.log(chalk8.bold(`
2683
+ console.log(chalk7.bold(`
2101
2684
  Restarting ${all.length} processes...
2102
2685
  `));
2103
2686
  for (const proc of all) {
2104
2687
  try {
2105
- console.log(chalk8.yellow(` \u21BB Restarting ${proc.name}...`));
2688
+ console.log(chalk7.yellow(` \u21BB Restarting ${proc.name}...`));
2106
2689
  await handleRun({
2107
2690
  action: "run",
2108
2691
  name: proc.name,
@@ -2110,10 +2693,10 @@ async function run2() {
2110
2693
  remoteName: ""
2111
2694
  });
2112
2695
  } catch (err) {
2113
- console.error(chalk8.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
2696
+ console.error(chalk7.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
2114
2697
  }
2115
2698
  }
2116
- console.log(chalk8.green(`
2699
+ console.log(chalk7.green(`
2117
2700
  \u2713 All processes restarted.
2118
2701
  `));
2119
2702
  return;
@@ -2125,22 +2708,22 @@ async function run2() {
2125
2708
  error("No processes registered.");
2126
2709
  return;
2127
2710
  }
2128
- console.log(chalk8.bold(`
2711
+ console.log(chalk7.bold(`
2129
2712
  Stopping ${all.length} processes...
2130
2713
  `));
2131
2714
  for (const proc of all) {
2132
2715
  try {
2133
2716
  if (await isProcessRunning(proc.pid)) {
2134
- console.log(chalk8.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
2717
+ console.log(chalk7.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
2135
2718
  await handleStop(proc.name);
2136
2719
  } else {
2137
- console.log(chalk8.gray(` \u25CB ${proc.name} already stopped`));
2720
+ console.log(chalk7.gray(` \u25CB ${proc.name} already stopped`));
2138
2721
  }
2139
2722
  } catch (err) {
2140
- console.error(chalk8.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
2723
+ console.error(chalk7.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
2141
2724
  }
2142
2725
  }
2143
- console.log(chalk8.green(`
2726
+ console.log(chalk7.green(`
2144
2727
  \u2713 All processes stopped.
2145
2728
  `));
2146
2729
  return;
@@ -2201,6 +2784,13 @@ async function run2() {
2201
2784
  });
2202
2785
  return;
2203
2786
  }
2787
+ if (name === "list") {
2788
+ await showAll({
2789
+ json: values.json,
2790
+ filter: values.filter
2791
+ });
2792
+ return;
2793
+ }
2204
2794
  if (name) {
2205
2795
  if (!values.command && !values.directory) {
2206
2796
  await showDetails(name);
@@ -2230,5 +2820,8 @@ async function run2() {
2230
2820
  }
2231
2821
  }
2232
2822
  run2().catch((err) => {
2233
- error(err);
2823
+ if (err.name !== "BgrunError") {
2824
+ console.error(err);
2825
+ }
2826
+ process.exit(1);
2234
2827
  });