bgrun 3.11.0 → 3.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,12 @@ 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
+ return false;
84
+ }
38
85
  } else {
39
86
  const result = await $`ps -p ${pid}`.nothrow().text();
40
87
  return result.includes(`${pid}`);
@@ -66,7 +113,7 @@ async function isDockerContainerRunning(command) {
66
113
  async function getChildPids(pid) {
67
114
  try {
68
115
  if (isWindows()) {
69
- const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
116
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
70
117
  return result.split(`
71
118
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
72
119
  } else {
@@ -125,6 +172,35 @@ async function isPortFree(port) {
125
172
  return true;
126
173
  }
127
174
  }
175
+ async function getPortInfo(port) {
176
+ try {
177
+ if (isWindows()) {
178
+ const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
179
+ for (const line of result.split(`
180
+ `)) {
181
+ const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
182
+ if (match) {
183
+ const pid = parseInt(match[2]);
184
+ if (pid > 0 && await isProcessRunning(pid)) {
185
+ const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
186
+ return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
187
+ }
188
+ }
189
+ }
190
+ return { inUse: false };
191
+ } else {
192
+ const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
193
+ const lines = result.trim().split(`
194
+ `).filter((l) => l.trim());
195
+ if (lines.length > 1) {
196
+ return { inUse: true };
197
+ }
198
+ return { inUse: false };
199
+ }
200
+ } catch {
201
+ return { inUse: false };
202
+ }
203
+ }
128
204
  async function waitForPortFree(port, timeoutMs = 5000) {
129
205
  const startTime = Date.now();
130
206
  const pollInterval = 300;
@@ -188,12 +264,12 @@ function getShellCommand(command) {
188
264
  }
189
265
  async function findChildPid(parentPid) {
190
266
  let currentPid = parentPid;
191
- const maxDepth = 5;
267
+ const maxDepth = 2;
192
268
  for (let depth = 0;depth < maxDepth; depth++) {
193
269
  try {
194
270
  let childPids = [];
195
271
  if (isWindows()) {
196
- const result = await $`powershell -Command "Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId"`.nothrow().text();
272
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
197
273
  childPids = result.split(`
198
274
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
199
275
  } else {
@@ -201,9 +277,8 @@ async function findChildPid(parentPid) {
201
277
  childPids = result.trim().split(`
202
278
  `).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
203
279
  }
204
- if (childPids.length === 0) {
280
+ if (childPids.length === 0)
205
281
  break;
206
- }
207
282
  currentPid = childPids[0];
208
283
  } catch {
209
284
  break;
@@ -211,6 +286,112 @@ async function findChildPid(parentPid) {
211
286
  }
212
287
  return currentPid;
213
288
  }
289
+ async function reconcileProcessPids(processes, deadPids) {
290
+ return await plat.measure("Reconcile PIDs", async () => {
291
+ const result = new Map;
292
+ const needsReconciliation = processes.filter((p) => deadPids.has(p.pid) && p.pid > 0);
293
+ if (needsReconciliation.length === 0)
294
+ return result;
295
+ try {
296
+ let runningProcs = [];
297
+ if (isWindows()) {
298
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`, 5000);
299
+ for (const line of output.split(`
300
+ `)) {
301
+ const sepIdx = line.indexOf("|");
302
+ if (sepIdx === -1)
303
+ continue;
304
+ const pid = parseInt(line.substring(0, sepIdx).trim());
305
+ const cmdLine = line.substring(sepIdx + 1).trim();
306
+ if (!isNaN(pid) && pid > 0 && cmdLine) {
307
+ runningProcs.push({ pid, cmdLine });
308
+ }
309
+ }
310
+ } else {
311
+ const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
312
+ for (const line of psOutput.trim().split(`
313
+ `)) {
314
+ const match = line.trim().match(/^(\d+)\s+(.+)/);
315
+ if (match) {
316
+ runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
317
+ }
318
+ }
319
+ }
320
+ for (const proc of needsReconciliation) {
321
+ const cmdParts = proc.command.split(/\s+/);
322
+ const workdirParts = proc.workdir.replace(/\\/g, "/").split("/").filter(Boolean);
323
+ const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || "";
324
+ let bestMatch = null;
325
+ let ambiguous = false;
326
+ for (const running of runningProcs) {
327
+ const cmdLower = running.cmdLine.toLowerCase();
328
+ let score = 0;
329
+ for (const part of cmdParts) {
330
+ if (part.length > 2 && cmdLower.includes(part.toLowerCase()))
331
+ score++;
332
+ }
333
+ if (workdirLast && cmdLower.includes(workdirLast))
334
+ score += 3;
335
+ if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, "/")))
336
+ score += 5;
337
+ if (cmdLower.includes(proc.workdir.toLowerCase()))
338
+ score += 5;
339
+ if (score < 4)
340
+ continue;
341
+ if (!bestMatch || score > bestMatch.score) {
342
+ ambiguous = false;
343
+ bestMatch = { pid: running.pid, score };
344
+ } else if (score === bestMatch.score) {
345
+ ambiguous = true;
346
+ }
347
+ }
348
+ if (bestMatch && !ambiguous) {
349
+ result.set(proc.name, bestMatch.pid);
350
+ runningProcs = runningProcs.filter((p) => p.pid !== bestMatch.pid);
351
+ }
352
+ }
353
+ } catch {}
354
+ return result;
355
+ }) ?? new Map;
356
+ }
357
+ async function findPidByPort(port, maxWaitMs = 8000) {
358
+ const start = Date.now();
359
+ const pollMs = 500;
360
+ while (Date.now() - start < maxWaitMs) {
361
+ try {
362
+ if (isWindows()) {
363
+ const result = await $`netstat -ano`.nothrow().quiet().text();
364
+ for (const line of result.split(`
365
+ `)) {
366
+ if (line.includes(`:${port}`) && line.includes("LISTENING")) {
367
+ const parts = line.trim().split(/\s+/);
368
+ const pid = parseInt(parts[parts.length - 1]);
369
+ if (!isNaN(pid) && pid > 0)
370
+ return pid;
371
+ }
372
+ }
373
+ } else {
374
+ try {
375
+ const result2 = await $`ss -tlnp`.nothrow().quiet().text();
376
+ for (const line of result2.split(`
377
+ `)) {
378
+ if (line.includes(`:${port}`)) {
379
+ const pidMatch = line.match(/pid=(\d+)/);
380
+ if (pidMatch)
381
+ return parseInt(pidMatch[1]);
382
+ }
383
+ }
384
+ } catch {}
385
+ const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
386
+ const pid = parseInt(result.trim());
387
+ if (!isNaN(pid) && pid > 0)
388
+ return pid;
389
+ }
390
+ } catch {}
391
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
392
+ }
393
+ return null;
394
+ }
214
395
  async function readFileTail(filePath, lines) {
215
396
  return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
216
397
  try {
@@ -227,6 +408,13 @@ async function readFileTail(filePath, lines) {
227
408
  }
228
409
  }) ?? "";
229
410
  }
411
+ function copyFile(src, dest) {
412
+ fs.copyFileSync(src, dest);
413
+ }
414
+ async function getProcessMemory(pid) {
415
+ const map = await getProcessBatchResources([pid]);
416
+ return map.get(pid)?.memory || 0;
417
+ }
230
418
  async function getProcessBatchResources(pids) {
231
419
  if (pids.length === 0)
232
420
  return new Map;
@@ -235,28 +423,16 @@ async function getProcessBatchResources(pids) {
235
423
  const pidSet = new Set(pids);
236
424
  try {
237
425
  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("--"))
426
+ const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
427
+ for (const line of output.split(`
428
+ `)) {
429
+ const sepIdx = line.indexOf("|");
430
+ if (sepIdx === -1)
244
431
  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
- }
432
+ const pid = parseInt(line.substring(0, sepIdx).trim());
433
+ const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
434
+ if (!isNaN(pid) && pidSet.has(pid)) {
435
+ resourceMap.set(pid, { memory, cpu: 0 });
260
436
  }
261
437
  }
262
438
  } else {
@@ -333,7 +509,6 @@ var init_platform = __esm(() => {
333
509
 
334
510
  // src/utils.ts
335
511
  import * as fs2 from "fs";
336
- import chalk from "chalk";
337
512
  function parseEnvString(envString) {
338
513
  const env = {};
339
514
  envString.split(",").forEach((pair) => {
@@ -351,8 +526,8 @@ function calculateRuntime(startTime) {
351
526
  }
352
527
  async function getVersion() {
353
528
  try {
354
- const { join } = await import("path");
355
- const pkgPath = join(import.meta.dir, "../package.json");
529
+ const { join: join2 } = await import("path");
530
+ const pkgPath = join2(import.meta.dir, "../package.json");
356
531
  const pkg = await Bun.file(pkgPath).json();
357
532
  return pkg.version || "0.0.0";
358
533
  } catch {
@@ -361,8 +536,7 @@ async function getVersion() {
361
536
  }
362
537
  function validateDirectory(directory) {
363
538
  if (!directory || !fs2.existsSync(directory)) {
364
- console.log(chalk.red("\u274C Error: 'directory' must be a valid path."));
365
- process.exit(1);
539
+ throw new Error(`Directory not found or invalid: '${directory}'`);
366
540
  }
367
541
  }
368
542
  function tailFile(path, prefix, colorFn, lines) {
@@ -417,21 +591,31 @@ var exports_db = {};
417
591
  __export(exports_db, {
418
592
  updateProcessPid: () => updateProcessPid,
419
593
  updateProcessEnv: () => updateProcessEnv,
594
+ saveTemplate: () => saveTemplate,
420
595
  retryDatabaseOperation: () => retryDatabaseOperation,
421
596
  removeProcessByName: () => removeProcessByName,
422
597
  removeProcess: () => removeProcess,
423
598
  removeAllProcesses: () => removeAllProcesses,
424
599
  insertProcess: () => insertProcess,
600
+ getTemplate: () => getTemplate,
601
+ getRecentHistory: () => getRecentHistory,
602
+ getProcessHistory: () => getProcessHistory,
425
603
  getProcess: () => getProcess,
426
604
  getDbInfo: () => getDbInfo,
605
+ getAllTemplates: () => getAllTemplates,
427
606
  getAllProcesses: () => getAllProcesses,
607
+ deleteTemplate: () => deleteTemplate,
428
608
  dbPath: () => dbPath,
429
609
  db: () => db,
610
+ clearOldHistory: () => clearOldHistory,
430
611
  bgrHome: () => bgrHome,
431
- ProcessSchema: () => ProcessSchema
612
+ addHistoryEntry: () => addHistoryEntry,
613
+ TemplateSchema: () => TemplateSchema,
614
+ ProcessSchema: () => ProcessSchema,
615
+ HistorySchema: () => HistorySchema
432
616
  });
433
617
  import { Database, z } from "sqlite-zod-orm";
434
- import { join } from "path";
618
+ import { join as join2 } from "path";
435
619
  var {sleep } = globalThis.Bun;
436
620
  import { existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
437
621
  function getProcess(name) {
@@ -476,6 +660,61 @@ function updateProcessEnv(name, envJson) {
476
660
  db.process.update(proc.id, { env: envJson });
477
661
  }
478
662
  }
663
+ function getAllTemplates() {
664
+ return db.template.select().all();
665
+ }
666
+ function getTemplate(name) {
667
+ return db.template.select().where({ name }).limit(1).get() || null;
668
+ }
669
+ function saveTemplate(data) {
670
+ const existing = db.template.select().where({ name: data.name }).limit(1).get();
671
+ if (existing) {
672
+ db.template.update(existing.id, {
673
+ command: data.command,
674
+ workdir: data.workdir || "",
675
+ env: data.env || "",
676
+ group: data.group || ""
677
+ });
678
+ } else {
679
+ db.template.insert({
680
+ name: data.name,
681
+ command: data.command,
682
+ workdir: data.workdir || "",
683
+ env: data.env || "",
684
+ group: data.group || ""
685
+ });
686
+ }
687
+ }
688
+ function deleteTemplate(name) {
689
+ const tmpl = db.template.select().where({ name }).limit(1).get();
690
+ if (tmpl) {
691
+ db.template.delete(tmpl.id);
692
+ }
693
+ }
694
+ function getProcessHistory(name, limit = 50) {
695
+ return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
696
+ }
697
+ function addHistoryEntry(processName, event, pid, metadata = {}) {
698
+ return db.history.insert({
699
+ process_name: processName,
700
+ event,
701
+ pid,
702
+ metadata: JSON.stringify(metadata)
703
+ });
704
+ }
705
+ function getRecentHistory(limit = 100) {
706
+ return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
707
+ }
708
+ function clearOldHistory(daysToKeep = 30) {
709
+ const cutoff = new Date;
710
+ cutoff.setDate(cutoff.getDate() - daysToKeep);
711
+ const cutoffStr = cutoff.toISOString();
712
+ const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
713
+ for (const entry of oldEntries) {
714
+ db.history.delete(entry.id);
715
+ }
716
+ return oldEntries.length;
717
+ }
479
718
  function getDbInfo() {
480
719
  return {
481
720
  dbPath,
@@ -498,7 +737,7 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
498
737
  }
499
738
  throw new Error("Max retries reached for database operation");
500
739
  }
501
- var ProcessSchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
740
+ var ProcessSchema, TemplateSchema, HistorySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
502
741
  var init_db = __esm(() => {
503
742
  init_platform();
504
743
  ProcessSchema = z.object({
@@ -510,15 +749,31 @@ var init_db = __esm(() => {
510
749
  configPath: z.string().default(""),
511
750
  stdout_path: z.string(),
512
751
  stderr_path: z.string(),
513
- timestamp: z.string().default(() => new Date().toISOString())
752
+ timestamp: z.string().default(() => new Date().toISOString()),
753
+ group: z.string().default("")
754
+ });
755
+ TemplateSchema = z.object({
756
+ name: z.string(),
757
+ command: z.string(),
758
+ workdir: z.string().default(""),
759
+ env: z.string().default(""),
760
+ group: z.string().default(""),
761
+ created_at: z.string().default(() => new Date().toISOString())
762
+ });
763
+ HistorySchema = z.object({
764
+ process_name: z.string(),
765
+ event: z.string(),
766
+ pid: z.number().optional(),
767
+ timestamp: z.string().default(() => new Date().toISOString()),
768
+ metadata: z.string().default("")
514
769
  });
515
770
  homePath = getHomeDir();
516
- bgrDir = join(homePath, ".bgr");
771
+ bgrDir = join2(homePath, ".bgr");
517
772
  ensureDir(bgrDir);
518
773
  dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
519
- dbPath = join(bgrDir, dbFilename);
774
+ dbPath = join2(bgrDir, dbFilename);
520
775
  bgrHome = bgrDir;
521
- legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
776
+ legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
522
777
  if (!existsSync3(dbPath) && existsSync3(legacyDbPath)) {
523
778
  try {
524
779
  copyFileSync2(legacyDbPath, dbPath);
@@ -526,17 +781,21 @@ var init_db = __esm(() => {
526
781
  } catch (e) {}
527
782
  }
528
783
  db = new Database(dbPath, {
529
- process: ProcessSchema
784
+ process: ProcessSchema,
785
+ template: TemplateSchema,
786
+ history: HistorySchema
530
787
  }, {
531
788
  indexes: {
532
- process: ["name", "timestamp", "pid"]
789
+ process: ["name", "timestamp", "pid"],
790
+ template: ["name"],
791
+ history: ["process_name", "timestamp"]
533
792
  }
534
793
  });
535
794
  });
536
795
 
537
796
  // src/logger.ts
538
797
  import boxen from "boxen";
539
- import chalk2 from "chalk";
798
+ import chalk from "chalk";
540
799
  function announce(message, title) {
541
800
  console.log(boxen(message, {
542
801
  padding: 1,
@@ -549,7 +808,7 @@ function announce(message, title) {
549
808
  }
550
809
  function error(message) {
551
810
  const text = message instanceof Error ? message.stack || message.message : String(message);
552
- console.error(boxen(chalk2.red(text), {
811
+ console.error(boxen(chalk.red(text), {
553
812
  padding: 1,
554
813
  margin: 1,
555
814
  borderColor: "red",
@@ -557,9 +816,17 @@ function error(message) {
557
816
  titleAlignment: "center",
558
817
  borderStyle: "double"
559
818
  }));
560
- process.exit(1);
819
+ throw new BgrunError(text);
561
820
  }
562
- var init_logger = () => {};
821
+ var BgrunError;
822
+ var init_logger = __esm(() => {
823
+ BgrunError = class BgrunError extends Error {
824
+ constructor(message) {
825
+ super(message);
826
+ this.name = "BgrunError";
827
+ }
828
+ };
829
+ });
563
830
 
564
831
  // src/config.ts
565
832
  function formatEnvKey(key) {
@@ -684,10 +951,10 @@ var init_deps = __esm(() => {
684
951
  // src/commands/run.ts
685
952
  var {$: $2 } = globalThis.Bun;
686
953
  var {sleep: sleep2 } = globalThis.Bun;
687
- import { join as join2 } from "path";
954
+ import { join as join3 } from "path";
688
955
  import { createMeasure as createMeasure2 } from "measure-fn";
689
956
  async function handleRun(options) {
690
- const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
957
+ const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
691
958
  const existingProcess = name ? getProcess(name) : null;
692
959
  if (name && existingProcess) {
693
960
  const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
@@ -708,7 +975,7 @@ async function handleRun(options) {
708
975
  const finalDirectory2 = directory || existingProcess.workdir;
709
976
  validateDirectory(finalDirectory2);
710
977
  $2.cwd(finalDirectory2);
711
- if (fetch) {
978
+ if (fetch2) {
712
979
  if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
713
980
  error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
714
981
  }
@@ -758,9 +1025,15 @@ async function handleRun(options) {
758
1025
  if (cmdToMatch) {
759
1026
  await run.measure("Zombie sweep", async () => {
760
1027
  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();
1028
+ const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1029
+ const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1030
+ if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1031
+ return;
1032
+ }
1033
+ const currentPid = process.pid;
1034
+ 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
1035
  const zombiePids = result.split(`
763
- `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0);
1036
+ `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
764
1037
  for (const zPid of zombiePids) {
765
1038
  await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
766
1039
  }
@@ -781,6 +1054,9 @@ async function handleRun(options) {
781
1054
  const finalCommand = command || existingProcess.command;
782
1055
  const finalDirectory = directory || existingProcess?.workdir;
783
1056
  let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1057
+ if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1058
+ finalEnv.BGR_KEEP_ALIVE = "true";
1059
+ }
784
1060
  let finalConfigPath;
785
1061
  if (configPath !== undefined) {
786
1062
  finalConfigPath = configPath;
@@ -790,7 +1066,7 @@ async function handleRun(options) {
790
1066
  finalConfigPath = ".config.toml";
791
1067
  }
792
1068
  if (finalConfigPath) {
793
- const fullConfigPath = join2(finalDirectory, finalConfigPath);
1069
+ const fullConfigPath = join3(finalDirectory, finalConfigPath);
794
1070
  if (await Bun.file(fullConfigPath).exists()) {
795
1071
  const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
796
1072
  try {
@@ -808,9 +1084,9 @@ async function handleRun(options) {
808
1084
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
809
1085
  }
810
1086
  }
811
- const stdoutPath = stdout || existingProcess?.stdout_path || join2(homePath2, ".bgr", `${name}-out.txt`);
1087
+ const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
812
1088
  Bun.write(stdoutPath, "");
813
- const stderrPath = stderr || existingProcess?.stderr_path || join2(homePath2, ".bgr", `${name}-err.txt`);
1089
+ const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
814
1090
  Bun.write(stderrPath, "");
815
1091
  const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
816
1092
  const newProcess = Bun.spawn(getShellCommand(finalCommand), {
@@ -854,7 +1130,7 @@ __export(exports_log_rotation, {
854
1130
  rotateLogFile: () => rotateLogFile,
855
1131
  rotateAllLogs: () => rotateAllLogs
856
1132
  });
857
- import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
1133
+ import { existsSync as existsSync7, statSync as statSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
858
1134
  function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAULT_KEEP_LINES) {
859
1135
  try {
860
1136
  if (!existsSync7(filePath))
@@ -870,7 +1146,7 @@ function rotateLogFile(filePath, maxBytes = DEFAULT_MAX_BYTES, keepLines = DEFAU
870
1146
  const truncated = lines.slice(-keepLines);
871
1147
  const header = `--- [bgrun] Log rotated at ${new Date().toISOString()} (was ${lines.length} lines, ${formatBytes(stat.size)}) ---
872
1148
  `;
873
- writeFileSync(filePath, header + truncated.join(`
1149
+ writeFileSync2(filePath, header + truncated.join(`
874
1150
  `));
875
1151
  return true;
876
1152
  } catch {
@@ -922,22 +1198,87 @@ var init_log_rotation = __esm(() => {
922
1198
  var exports_server = {};
923
1199
  __export(exports_server, {
924
1200
  startServer: () => startServer,
925
- guardRestartCounts: () => guardRestartCounts
1201
+ guardRestartCounts: () => guardRestartCounts,
1202
+ guardEvents: () => guardEvents
926
1203
  });
927
1204
  import path2 from "path";
1205
+ async function cleanupPort(port) {
1206
+ if (process.platform !== "win32")
1207
+ return port;
1208
+ try {
1209
+ const proc = Bun.spawn([
1210
+ "powershell",
1211
+ "-NoProfile",
1212
+ "-Command",
1213
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1214
+ ], { stdout: "pipe", stderr: "pipe" });
1215
+ const text = await new Response(proc.stdout).text();
1216
+ const pid = parseInt(text.trim(), 10);
1217
+ if (!pid || pid === process.pid)
1218
+ return port;
1219
+ const checkProc = Bun.spawn([
1220
+ "powershell",
1221
+ "-NoProfile",
1222
+ "-Command",
1223
+ `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
1224
+ ], { stdout: "pipe", stderr: "pipe" });
1225
+ const checkText = await new Response(checkProc.stdout).text();
1226
+ if (checkText.trim()) {
1227
+ console.log(`[server] Killing PID ${pid} holding port ${port}`);
1228
+ Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
1229
+ await Bun.sleep(1000);
1230
+ return port;
1231
+ } else {
1232
+ const fallback = port + 1;
1233
+ console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
1234
+ return fallback;
1235
+ }
1236
+ } catch {
1237
+ return port;
1238
+ }
1239
+ }
928
1240
  async function startServer() {
929
1241
  const { start } = await import("melina");
930
1242
  const appDir = path2.join(import.meta.dir, "../dashboard/app");
931
- const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
1243
+ const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
1244
+ _originalPort = requestedPort;
1245
+ const resolvedPort = await cleanupPort(requestedPort);
1246
+ _currentPort = resolvedPort;
1247
+ const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
932
1248
  await start({
933
1249
  appDir,
934
1250
  defaultTitle: "bgrun Dashboard - Process Manager",
935
1251
  globalCss: path2.join(appDir, "globals.css"),
936
- ...explicitPort !== undefined && { port: explicitPort }
1252
+ ...needsExplicitPort && { port: resolvedPort }
937
1253
  });
938
1254
  startGuard();
939
1255
  const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
940
1256
  startLogRotation2(() => getAllProcesses());
1257
+ if (resolvedPort !== requestedPort) {
1258
+ startStickyPortChecker();
1259
+ }
1260
+ }
1261
+ function startStickyPortChecker() {
1262
+ const CHECK_INTERVAL_MS = 60000;
1263
+ console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
1264
+ setInterval(async () => {
1265
+ if (_currentPort === _originalPort)
1266
+ return;
1267
+ try {
1268
+ const proc = Bun.spawn([
1269
+ "powershell",
1270
+ "-NoProfile",
1271
+ "-Command",
1272
+ `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1273
+ ], { stdout: "pipe", stderr: "pipe" });
1274
+ const text = await new Response(proc.stdout).text();
1275
+ const pid = parseInt(text.trim(), 10);
1276
+ if (!pid) {
1277
+ console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
1278
+ _currentPort = _originalPort;
1279
+ }
1280
+ } catch {}
1281
+ }, CHECK_INTERVAL_MS);
941
1282
  }
942
1283
  function startGuard() {
943
1284
  console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
@@ -959,6 +1300,7 @@ function startGuard() {
959
1300
  if (now < nextRestart)
960
1301
  continue;
961
1302
  console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1303
+ let success = false;
962
1304
  try {
963
1305
  await handleRun({
964
1306
  action: "run",
@@ -966,9 +1308,16 @@ function startGuard() {
966
1308
  force: true,
967
1309
  remoteName: ""
968
1310
  });
1311
+ success = true;
969
1312
  const prevCount = guardRestartCounts.get(proc.name) || 0;
970
1313
  const newCount = prevCount + 1;
971
1314
  guardRestartCounts.set(proc.name, newCount);
1315
+ try {
1316
+ addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1317
+ } catch {}
1318
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1319
+ if (guardEvents.length > 100)
1320
+ guardEvents.pop();
972
1321
  if (newCount > 5) {
973
1322
  const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
974
1323
  guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
@@ -978,6 +1327,9 @@ function startGuard() {
978
1327
  }
979
1328
  } catch (err) {
980
1329
  console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1330
+ guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1331
+ if (guardEvents.length > 100)
1332
+ guardEvents.pop();
981
1333
  }
982
1334
  } else {
983
1335
  const prevCount = guardRestartCounts.get(proc.name) || 0;
@@ -995,7 +1347,7 @@ function startGuard() {
995
1347
  }
996
1348
  }, GUARD_INTERVAL_MS);
997
1349
  }
998
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime;
1350
+ var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
999
1351
  var init_server = __esm(() => {
1000
1352
  init_db();
1001
1353
  init_platform();
@@ -1007,8 +1359,11 @@ var init_server = __esm(() => {
1007
1359
  _g.__bgrGuardRestartCounts = new Map;
1008
1360
  if (!_g.__bgrGuardNextRestartTime)
1009
1361
  _g.__bgrGuardNextRestartTime = new Map;
1362
+ if (!_g.__bgrGuardEvents)
1363
+ _g.__bgrGuardEvents = [];
1010
1364
  guardRestartCounts = _g.__bgrGuardRestartCounts;
1011
1365
  guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1366
+ guardEvents = _g.__bgrGuardEvents;
1012
1367
  });
1013
1368
 
1014
1369
  // src/guard.ts
@@ -1016,6 +1371,38 @@ var exports_guard = {};
1016
1371
  __export(exports_guard, {
1017
1372
  startGuardLoop: () => startGuardLoop
1018
1373
  });
1374
+ import { createHmac } from "crypto";
1375
+ async function notifyWebhook(event, name, details) {
1376
+ if (!WEBHOOK_URL)
1377
+ return;
1378
+ try {
1379
+ const payload = JSON.stringify({
1380
+ event,
1381
+ process: name,
1382
+ timestamp: new Date().toISOString(),
1383
+ ...details
1384
+ });
1385
+ const headers = {
1386
+ "Content-Type": "application/json",
1387
+ "User-Agent": "bgrun-guard/1.0"
1388
+ };
1389
+ if (WEBHOOK_SECRET) {
1390
+ const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
1391
+ headers["X-BGR-Signature"] = `sha256=${sig}`;
1392
+ }
1393
+ const controller = new AbortController;
1394
+ const timeout = setTimeout(() => controller.abort(), 5000);
1395
+ await fetch(WEBHOOK_URL, {
1396
+ method: "POST",
1397
+ headers,
1398
+ body: payload,
1399
+ signal: controller.signal
1400
+ });
1401
+ clearTimeout(timeout);
1402
+ } catch (err) {
1403
+ console.error(`[guard] Webhook failed: ${err.message}`);
1404
+ }
1405
+ }
1019
1406
  async function restartProcess(name) {
1020
1407
  try {
1021
1408
  await handleRun({
@@ -1064,6 +1451,7 @@ async function guardCycle() {
1064
1451
  continue;
1065
1452
  }
1066
1453
  console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
1454
+ notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
1067
1455
  const success = await restartProcess(proc.name);
1068
1456
  if (success) {
1069
1457
  const count = (state.restartCounts.get(proc.name) || 0) + 1;
@@ -1077,6 +1465,9 @@ async function guardCycle() {
1077
1465
  console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
1078
1466
  }
1079
1467
  restarted++;
1468
+ notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
1469
+ } else {
1470
+ notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
1080
1471
  }
1081
1472
  } else if (alive) {
1082
1473
  const count = state.restartCounts.get(proc.name) || 0;
@@ -1111,17 +1502,20 @@ async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
1111
1502
  console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
1112
1503
  console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
1113
1504
  console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
1505
+ console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
1114
1506
  console.log(`[guard] Started: ${new Date().toLocaleString()}`);
1115
1507
  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
1508
  await guardCycle();
1117
1509
  setInterval(guardCycle, interval);
1118
1510
  }
1119
- var DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1511
+ var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1120
1512
  var init_guard = __esm(() => {
1121
1513
  init_db();
1122
1514
  init_platform();
1123
1515
  init_run();
1124
1516
  init_utils();
1517
+ WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
1518
+ WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
1125
1519
  MAX_BACKOFF_MS = 5 * 60000;
1126
1520
  state = {
1127
1521
  restartCounts: new Map,
@@ -1136,10 +1530,10 @@ init_run();
1136
1530
  import { parseArgs } from "util";
1137
1531
 
1138
1532
  // src/commands/list.ts
1139
- import chalk4 from "chalk";
1533
+ import chalk3 from "chalk";
1140
1534
 
1141
1535
  // src/table.ts
1142
- import chalk3 from "chalk";
1536
+ import chalk2 from "chalk";
1143
1537
  function getTerminalWidth() {
1144
1538
  return process.stdout.columns || 120;
1145
1539
  }
@@ -1221,7 +1615,7 @@ function renderBorder(widths, padding, style) {
1221
1615
  function renderHorizontalTable(rows, columns, options = {}) {
1222
1616
  const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
1223
1617
  if (rows.length === 0)
1224
- return { table: chalk3.gray("No data to display"), truncatedIndices: [] };
1618
+ return { table: chalk2.gray("No data to display"), truncatedIndices: [] };
1225
1619
  const borderChars = {
1226
1620
  rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
1227
1621
  single: ["\u250C", "\u252C", "\u2510", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2514", "\u2534", "\u2518"],
@@ -1237,7 +1631,7 @@ function renderHorizontalTable(rows, columns, options = {}) {
1237
1631
  if (borderStyle !== "none")
1238
1632
  lines.push(renderBorder(widthArray, padding, [tl, tc, tr, h]));
1239
1633
  if (showHeaders) {
1240
- const headerCells = columns.map((col, i) => chalk3.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
1634
+ const headerCells = columns.map((col, i) => chalk2.bold(truncateString(col.header, widthArray[i]).padEnd(widthArray[i])));
1241
1635
  lines.push(`${v}${cellPadding}${headerCells.join(`${cellPadding}${v}${cellPadding}`)}${cellPadding}${v}`);
1242
1636
  if (borderStyle !== "none")
1243
1637
  lines.push(renderBorder(widthArray, padding, [ml, mc, mr, h]));
@@ -1266,10 +1660,10 @@ function renderVerticalTree(rows, columns) {
1266
1660
  if (index > 0)
1267
1661
  lines.push("");
1268
1662
  const name = row.name ? `'${row.name}'` : `(ID: ${row.id})`;
1269
- lines.push(chalk3.cyan(`\u25B6 ${name}`));
1663
+ lines.push(chalk2.cyan(`\u25B6 ${name}`));
1270
1664
  columns.forEach((col) => {
1271
1665
  const value = col.formatter ? col.formatter(row[col.key]) : String(row[col.key] || "");
1272
- lines.push(` \u251C\u2500 ${chalk3.gray(`${col.header}:`)} ${value}`);
1666
+ lines.push(` \u251C\u2500 ${chalk2.gray(`${col.header}:`)} ${value}`);
1273
1667
  });
1274
1668
  });
1275
1669
  return lines.join(`
@@ -1288,15 +1682,15 @@ function renderHybridTable(rows, columns, options = {}) {
1288
1682
  }
1289
1683
  function renderProcessTable(processes, options) {
1290
1684
  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) },
1685
+ { key: "id", header: "ID", formatter: (id) => chalk2.blue(id) },
1686
+ { key: "pid", header: "PID", formatter: (pid) => chalk2.yellow(pid) },
1687
+ { key: "name", header: "Name", formatter: (name) => chalk2.cyan.bold(name) },
1688
+ { key: "port", header: "Port", formatter: (port) => port === "-" ? chalk2.gray(port) : chalk2.hex("#FF6B6B")(port) },
1689
+ { key: "memory", header: "Memory", formatter: (mem) => mem === "-" ? chalk2.gray(mem) : chalk2.hex("#4ECDC4")(mem) },
1296
1690
  { key: "command", header: "Command" },
1297
- { key: "workdir", header: "Directory", formatter: (dir) => chalk3.gray(dir), truncator: truncatePath },
1691
+ { key: "workdir", header: "Directory", formatter: (dir) => chalk2.gray(dir), truncator: truncatePath },
1298
1692
  { key: "status", header: "Status" },
1299
- { key: "runtime", header: "Runtime", formatter: (runtime) => chalk3.magenta(runtime) }
1693
+ { key: "runtime", header: "Runtime", formatter: (runtime) => chalk2.magenta(runtime) }
1300
1694
  ];
1301
1695
  return renderHybridTable(processes, columns, options);
1302
1696
  }
@@ -1322,10 +1716,29 @@ async function showAll(opts) {
1322
1716
  const envVars = parseEnvString(proc.env);
1323
1717
  return envVars["BGR_GROUP"] === opts.filter;
1324
1718
  });
1719
+ const deadPids = new Set;
1720
+ const aliveCache = new Map;
1721
+ for (const proc of filtered) {
1722
+ const alive = await isProcessRunning(proc.pid, proc.command);
1723
+ aliveCache.set(proc.pid, alive);
1724
+ if (!alive && proc.pid > 0)
1725
+ deadPids.add(proc.pid);
1726
+ }
1727
+ if (deadPids.size > 0) {
1728
+ const reconciled = await reconcileProcessPids(filtered.map((p) => ({ name: p.name, pid: p.pid, command: p.command, workdir: p.workdir })), deadPids);
1729
+ for (const [name, newPid] of reconciled) {
1730
+ updateProcessPid(name, newPid);
1731
+ const proc = filtered.find((p) => p.name === name);
1732
+ if (proc) {
1733
+ proc.pid = newPid;
1734
+ aliveCache.set(newPid, true);
1735
+ }
1736
+ }
1737
+ }
1325
1738
  if (opts?.json) {
1326
1739
  const jsonData = [];
1327
1740
  for (const proc of filtered) {
1328
- const isRunning = await isProcessRunning(proc.pid, proc.command);
1741
+ const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1329
1742
  const envVars = parseEnvString(proc.env);
1330
1743
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1331
1744
  jsonData.push({
@@ -1343,7 +1756,7 @@ async function showAll(opts) {
1343
1756
  const allPids = filtered.map((p) => p.pid);
1344
1757
  const resourceMap = await getProcessBatchResources(allPids);
1345
1758
  for (const proc of filtered) {
1346
- const isRunning = await isProcessRunning(proc.pid, proc.command);
1759
+ const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1347
1760
  const runtime = calculateRuntime(proc.timestamp);
1348
1761
  const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1349
1762
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
@@ -1355,7 +1768,7 @@ async function showAll(opts) {
1355
1768
  memory: formatMemory(mem),
1356
1769
  command: proc.command,
1357
1770
  workdir: proc.workdir,
1358
- status: isRunning ? chalk4.green.bold("\u25CF Running") : chalk4.red.bold("\u25CB Stopped"),
1771
+ status: isRunning ? chalk3.green.bold("\u25CF Running") : chalk3.red.bold("\u25CB Stopped"),
1359
1772
  runtime
1360
1773
  });
1361
1774
  }
@@ -1375,7 +1788,7 @@ async function showAll(opts) {
1375
1788
  console.log(tableOutput);
1376
1789
  const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
1377
1790
  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`)})`));
1791
+ console.log(chalk3.cyan(`Total: ${tableData.length} processes (${chalk3.green(`${runningCount} running`)}, ${chalk3.red(`${stoppedCount} stopped`)})`));
1379
1792
  }
1380
1793
 
1381
1794
  // src/commands/cleanup.ts
@@ -1506,7 +1919,7 @@ init_utils();
1506
1919
  init_run();
1507
1920
  import * as fs4 from "fs";
1508
1921
  import path from "path";
1509
- import chalk5 from "chalk";
1922
+ import chalk4 from "chalk";
1510
1923
  async function handleWatch(options, logOptions) {
1511
1924
  let currentProcess = null;
1512
1925
  let isRestarting = false;
@@ -1517,7 +1930,7 @@ async function handleWatch(options, logOptions) {
1517
1930
  const isDead = !await isProcessRunning(proc.pid);
1518
1931
  if (!isDead)
1519
1932
  return false;
1520
- console.log(chalk5.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
1933
+ console.log(chalk4.yellow(`\uD83D\uDC80 Process '${options.name}' died immediately after ${reason}\u2014dumping logs:`));
1521
1934
  const readAndDump = (path2, color, label) => {
1522
1935
  try {
1523
1936
  if (fs4.existsSync(path2)) {
@@ -1531,14 +1944,14 @@ ${color(content)}
1531
1944
  }
1532
1945
  }
1533
1946
  } catch (err) {
1534
- console.warn(chalk5.gray(`Could not read ${label} log: ${err}`));
1947
+ console.warn(chalk4.gray(`Could not read ${label} log: ${err}`));
1535
1948
  }
1536
1949
  };
1537
1950
  if (logOptions.logType === "both" || logOptions.logType === "stdout") {
1538
- readAndDump(proc.stdout_path, chalk5.white, "\uD83D\uDCC4 Stdout");
1951
+ readAndDump(proc.stdout_path, chalk4.white, "\uD83D\uDCC4 Stdout");
1539
1952
  }
1540
1953
  if (logOptions.logType === "both" || logOptions.logType === "stderr") {
1541
- readAndDump(proc.stderr_path, chalk5.red, "\uD83D\uDCC4 Stderr");
1954
+ readAndDump(proc.stderr_path, chalk4.red, "\uD83D\uDCC4 Stderr");
1542
1955
  }
1543
1956
  return true;
1544
1957
  };
@@ -1579,29 +1992,29 @@ ${color(content)}
1579
1992
  const stops = [];
1580
1993
  if (!logOptions.showLogs || !currentProcess)
1581
1994
  return stops;
1582
- console.log(chalk5.gray(`
1995
+ console.log(chalk4.gray(`
1583
1996
  ` + "\u2500".repeat(50) + `
1584
1997
  `));
1585
1998
  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)));
1999
+ console.log(chalk4.green.bold(`\uD83D\uDCC4 Tailing stdout for ${options.name}:`));
2000
+ console.log(chalk4.gray("\u2550".repeat(50)));
1588
2001
  try {
1589
2002
  await waitForLogReady(currentProcess.stdout_path);
1590
2003
  } catch (err) {
1591
- console.warn(chalk5.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
2004
+ console.warn(chalk4.yellow(`\u26A0\uFE0F Stdout log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
1592
2005
  }
1593
- const stop = tailFile(currentProcess.stdout_path, "", chalk5.white, logOptions.lines);
2006
+ const stop = tailFile(currentProcess.stdout_path, "", chalk4.white, logOptions.lines);
1594
2007
  stops.push(stop);
1595
2008
  }
1596
2009
  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)));
2010
+ console.log(chalk4.red.bold(`\uD83D\uDCC4 Tailing stderr for ${options.name}:`));
2011
+ console.log(chalk4.gray("\u2550".repeat(50)));
1599
2012
  try {
1600
2013
  await waitForLogReady(currentProcess.stderr_path);
1601
2014
  } catch (err) {
1602
- console.warn(chalk5.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
2015
+ console.warn(chalk4.yellow(`\u26A0\uFE0F Stderr log not ready yet for ${options.name}\u2014starting tail anyway: ${err.message}`));
1603
2016
  }
1604
- const stop = tailFile(currentProcess.stderr_path, "", chalk5.red, logOptions.lines);
2017
+ const stop = tailFile(currentProcess.stderr_path, "", chalk4.red, logOptions.lines);
1605
2018
  stops.push(stop);
1606
2019
  }
1607
2020
  return stops;
@@ -1626,7 +2039,7 @@ ${color(content)}
1626
2039
  const died = await dumpLogsIfDead(currentProcess, restartReason);
1627
2040
  if (died) {
1628
2041
  if (lastRestartPath) {
1629
- console.log(chalk5.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
2042
+ console.log(chalk4.yellow(`\u26A0\uFE0F Compile error on change\u2014pausing restarts until manual fix.`));
1630
2043
  return;
1631
2044
  } else {
1632
2045
  error(`Failed to start process '${options.name}'. Aborting watch mode.`);
@@ -1639,7 +2052,7 @@ ${color(content)}
1639
2052
  } finally {
1640
2053
  isRestarting = false;
1641
2054
  if (currentProcess) {
1642
- console.log(chalk5.cyan(`
2055
+ console.log(chalk4.cyan(`
1643
2056
  \uD83D\uDC40 Watching for file changes in: ${currentProcess.workdir}`));
1644
2057
  }
1645
2058
  }
@@ -1659,7 +2072,7 @@ ${color(content)}
1659
2072
  }
1660
2073
  tailStops = await startTails();
1661
2074
  const workdir = currentProcess.workdir;
1662
- console.log(chalk5.cyan(`
2075
+ console.log(chalk4.cyan(`
1663
2076
  \uD83D\uDC40 Watching for file changes in: ${workdir}`));
1664
2077
  const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
1665
2078
  if (filename == null)
@@ -1672,7 +2085,7 @@ ${color(content)}
1672
2085
  debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
1673
2086
  });
1674
2087
  const cleanup = async () => {
1675
- console.log(chalk5.magenta(`
2088
+ console.log(chalk4.magenta(`
1676
2089
  SIGINT received...`));
1677
2090
  watcher.close();
1678
2091
  tailStops.forEach((stop) => stop());
@@ -1695,7 +2108,7 @@ SIGINT received...`));
1695
2108
  init_db();
1696
2109
  init_logger();
1697
2110
  init_platform();
1698
- import chalk6 from "chalk";
2111
+ import chalk5 from "chalk";
1699
2112
  import * as fs5 from "fs";
1700
2113
  async function showLogs(name, logType = "both", lines) {
1701
2114
  const proc = getProcess(name);
@@ -1704,17 +2117,17 @@ async function showLogs(name, logType = "both", lines) {
1704
2117
  return;
1705
2118
  }
1706
2119
  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)));
2120
+ console.log(chalk5.green.bold(`\uD83D\uDCC4 Stdout logs for ${name}:`));
2121
+ console.log(chalk5.gray("\u2550".repeat(50)));
1709
2122
  if (fs5.existsSync(proc.stdout_path)) {
1710
2123
  try {
1711
2124
  const output = await readFileTail(proc.stdout_path, lines);
1712
- console.log(output || chalk6.gray("(no output)"));
2125
+ console.log(output || chalk5.gray("(no output)"));
1713
2126
  } catch (err) {
1714
- console.log(chalk6.red(`Error reading stdout: ${err}`));
2127
+ console.log(chalk5.red(`Error reading stdout: ${err}`));
1715
2128
  }
1716
2129
  } else {
1717
- console.log(chalk6.gray("(log file not found)"));
2130
+ console.log(chalk5.gray("(log file not found)"));
1718
2131
  }
1719
2132
  if (logType === "both") {
1720
2133
  console.log(`
@@ -1722,17 +2135,17 @@ async function showLogs(name, logType = "both", lines) {
1722
2135
  }
1723
2136
  }
1724
2137
  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)));
2138
+ console.log(chalk5.red.bold(`\uD83D\uDCC4 Stderr logs for ${name}:`));
2139
+ console.log(chalk5.gray("\u2550".repeat(50)));
1727
2140
  if (fs5.existsSync(proc.stderr_path)) {
1728
2141
  try {
1729
2142
  const output = await readFileTail(proc.stderr_path, lines);
1730
- console.log(output || chalk6.gray("(no errors)"));
2143
+ console.log(output || chalk5.gray("(no errors)"));
1731
2144
  } catch (err) {
1732
- console.log(chalk6.red(`Error reading stderr: ${err}`));
2145
+ console.log(chalk5.red(`Error reading stderr: ${err}`));
1733
2146
  }
1734
2147
  } else {
1735
- console.log(chalk6.gray("(log file not found)"));
2148
+ console.log(chalk5.gray("(log file not found)"));
1736
2149
  }
1737
2150
  }
1738
2151
  }
@@ -1742,35 +2155,44 @@ init_logger();
1742
2155
  init_db();
1743
2156
  init_utils();
1744
2157
  init_platform();
1745
- import chalk7 from "chalk";
2158
+ import chalk6 from "chalk";
1746
2159
  async function showDetails(name) {
1747
2160
  const proc = getProcess(name);
1748
2161
  if (!proc) {
1749
2162
  error(`No process found named '${name}'`);
1750
2163
  return;
1751
2164
  }
1752
- const isRunning = await isProcessRunning(proc.pid, proc.command);
2165
+ let isRunning = await isProcessRunning(proc.pid, proc.command);
2166
+ if (!isRunning && proc.pid > 0) {
2167
+ const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
2168
+ const newPid = reconciled.get(proc.name);
2169
+ if (newPid) {
2170
+ updateProcessPid(proc.name, newPid);
2171
+ proc.pid = newPid;
2172
+ isRunning = true;
2173
+ }
2174
+ }
1753
2175
  const runtime = calculateRuntime(proc.timestamp);
1754
2176
  const envVars = parseEnvString(proc.env);
1755
2177
  const ports = isRunning ? await getProcessPorts(proc.pid) : [];
1756
- const portDisplay = ports.length > 0 ? ports.map((p) => chalk7.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
2178
+ const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
1757
2179
  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}
2180
+ ${chalk6.bold("Process Details:")}
2181
+ ${chalk6.gray("\u2550".repeat(50))}
2182
+ ${chalk6.cyan.bold("Name:")} ${proc.name}
2183
+ ${chalk6.yellow.bold("PID:")} ${proc.pid}${portDisplay ? `
2184
+ ${chalk6.hex("#FF6B6B").bold("Port:")} ${portDisplay}` : ""}
2185
+ ${chalk6.bold("Status:")} ${isRunning ? chalk6.green.bold("\u25CF Running") : chalk6.red.bold("\u25CB Stopped")}
2186
+ ${chalk6.magenta.bold("Runtime:")} ${runtime}
2187
+ ${chalk6.blue.bold("Working Directory:")} ${proc.workdir}
2188
+ ${chalk6.white.bold("Command:")} ${proc.command}
2189
+ ${chalk6.gray.bold("Config Path:")} ${proc.configPath}
2190
+ ${chalk6.green.bold("Stdout Path:")} ${proc.stdout_path}
2191
+ ${chalk6.red.bold("Stderr Path:")} ${proc.stderr_path}
1770
2192
 
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(`
2193
+ ${chalk6.bold("\uD83D\uDD27 Environment Variables:")}
2194
+ ${chalk6.gray("\u2550".repeat(50))}
2195
+ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${chalk6.yellow(value)}`).join(`
1774
2196
  `)}
1775
2197
  `;
1776
2198
  announce(details, `Process Details: ${name}`);
@@ -1781,8 +2203,8 @@ init_logger();
1781
2203
  init_platform();
1782
2204
  init_db();
1783
2205
  import dedent from "dedent";
1784
- import chalk8 from "chalk";
1785
- import { join as join3 } from "path";
2206
+ import chalk7 from "chalk";
2207
+ import { join as join4 } from "path";
1786
2208
  var {sleep: sleep3 } = globalThis.Bun;
1787
2209
  import { configure } from "measure-fn";
1788
2210
  if (!Bun.argv.includes("--_serve")) {
@@ -1790,15 +2212,55 @@ if (!Bun.argv.includes("--_serve")) {
1790
2212
  configure({ silent: true });
1791
2213
  }
1792
2214
  }
2215
+ function redirectConsoleToFiles() {
2216
+ const stdoutPath = Bun.env.BGR_STDOUT;
2217
+ const stderrPath = Bun.env.BGR_STDERR;
2218
+ if (!stdoutPath && !stderrPath)
2219
+ return;
2220
+ const { appendFileSync } = __require("fs");
2221
+ const stripAnsi2 = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
2222
+ const timestamp = () => new Date().toISOString().replace("T", " ").substring(0, 19);
2223
+ if (stdoutPath) {
2224
+ const origLog = console.log;
2225
+ const origWarn = console.warn;
2226
+ console.log = (...args) => {
2227
+ const line = `[${timestamp()}] ${stripAnsi2(args.map(String).join(" "))}
2228
+ `;
2229
+ try {
2230
+ appendFileSync(stdoutPath, line);
2231
+ } catch {}
2232
+ origLog.apply(console, args);
2233
+ };
2234
+ console.warn = (...args) => {
2235
+ const line = `[${timestamp()}] WARN: ${stripAnsi2(args.map(String).join(" "))}
2236
+ `;
2237
+ try {
2238
+ appendFileSync(stdoutPath, line);
2239
+ } catch {}
2240
+ origWarn.apply(console, args);
2241
+ };
2242
+ }
2243
+ if (stderrPath) {
2244
+ const origError = console.error;
2245
+ console.error = (...args) => {
2246
+ const line = `[${timestamp()}] ERROR: ${stripAnsi2(args.map(String).join(" "))}
2247
+ `;
2248
+ try {
2249
+ appendFileSync(stderrPath, line);
2250
+ } catch {}
2251
+ origError.apply(console, args);
2252
+ };
2253
+ }
2254
+ }
1793
2255
  async function showHelp() {
1794
2256
  const usage = dedent`
1795
- ${chalk8.bold("bgrun \u2014 Bun Background Runner")}
1796
- ${chalk8.gray("\u2550".repeat(50))}
2257
+ ${chalk7.bold("bgrun \u2014 Bun Background Runner")}
2258
+ ${chalk7.gray("\u2550".repeat(50))}
1797
2259
 
1798
- ${chalk8.yellow("Usage:")}
2260
+ ${chalk7.yellow("Usage:")}
1799
2261
  bgrun [name] [options]
1800
2262
 
1801
- ${chalk8.yellow("Commands:")}
2263
+ ${chalk7.yellow("Commands:")}
1802
2264
  bgrun List all processes
1803
2265
  bgrun [name] Show details for a process
1804
2266
  bgrun --dashboard Launch web dashboard (managed by bgrun)
@@ -1811,7 +2273,7 @@ async function showHelp() {
1811
2273
  bgrun --clean Remove all stopped processes
1812
2274
  bgrun --nuke Delete ALL processes
1813
2275
 
1814
- ${chalk8.yellow("Options:")}
2276
+ ${chalk7.yellow("Options:")}
1815
2277
  --name <string> Process name (required for new)
1816
2278
  --command <string> Process command (required for new)
1817
2279
  --directory <path> Working directory (required for new)
@@ -1831,7 +2293,7 @@ async function showHelp() {
1831
2293
  --port <number> Port for dashboard (default: 3000)
1832
2294
  --help Show this help message
1833
2295
 
1834
- ${chalk8.yellow("Examples:")}
2296
+ ${chalk7.yellow("Examples:")}
1835
2297
  bgrun --dashboard
1836
2298
  bgrun --name myapp --command "bun run dev" --directory . --watch
1837
2299
  bgrun myapp --logs --lines 50
@@ -1878,11 +2340,13 @@ async function run2() {
1878
2340
  allowPositionals: true
1879
2341
  });
1880
2342
  if (values["_serve"]) {
2343
+ redirectConsoleToFiles();
1881
2344
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
1882
2345
  await startServer2();
1883
2346
  return;
1884
2347
  }
1885
2348
  if (values["_guard-loop"]) {
2349
+ redirectConsoleToFiles();
1886
2350
  const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
1887
2351
  const intervalStr = positionals[0];
1888
2352
  const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
@@ -1892,7 +2356,7 @@ async function run2() {
1892
2356
  if (values.dashboard) {
1893
2357
  const dashboardName = "bgr-dashboard";
1894
2358
  const homePath3 = getHomeDir();
1895
- const bgrDir2 = join3(homePath3, ".bgr");
2359
+ const bgrDir2 = join4(homePath3, ".bgr");
1896
2360
  const requestedPort = values.port;
1897
2361
  const existing = getProcess(dashboardName);
1898
2362
  if (existing && await isProcessRunning(existing.pid)) {
@@ -1906,10 +2370,10 @@ async function run2() {
1906
2370
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
1907
2371
  announce(`Dashboard is already running (PID ${existing.pid})
1908
2372
 
1909
- ` + ` \uD83C\uDF10 ${chalk8.cyan(`http://localhost${portStr}`)}
2373
+ ` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
1910
2374
 
1911
- Use ${chalk8.yellow(`bgrun --stop ${dashboardName}`)} to stop it
1912
- Use ${chalk8.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
2375
+ Use ${chalk7.yellow(`bgrun --stop ${dashboardName}`)} to stop it
2376
+ Use ${chalk7.yellow(`bgrun --dashboard --force`)} to restart`, "BGR Dashboard");
1913
2377
  return;
1914
2378
  }
1915
2379
  if (existing) {
@@ -1927,35 +2391,39 @@ async function run2() {
1927
2391
  const scriptPath = resolve(process.argv[1]);
1928
2392
  const spawnCommand = `bun run ${scriptPath} --_serve`;
1929
2393
  const command = `bgrun --_serve`;
1930
- const stdoutPath = join3(bgrDir2, `${dashboardName}-out.txt`);
1931
- const stderrPath = join3(bgrDir2, `${dashboardName}-err.txt`);
2394
+ const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
2395
+ const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
1932
2396
  await Bun.write(stdoutPath, "");
1933
2397
  await Bun.write(stderrPath, "");
1934
2398
  const spawnEnv = { ...Bun.env };
1935
2399
  if (requestedPort) {
1936
2400
  spawnEnv.BUN_PORT = requestedPort;
1937
2401
  }
2402
+ spawnEnv.BGR_STDOUT = stdoutPath;
2403
+ spawnEnv.BGR_STDERR = stderrPath;
1938
2404
  const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
1939
2405
  if (!isNaN(targetPort) && targetPort > 0) {
1940
2406
  const portFree = await isPortFree(targetPort);
1941
2407
  if (!portFree) {
1942
- console.log(chalk8.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
2408
+ console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
1943
2409
  await killProcessOnPort(targetPort);
1944
2410
  const freed = await waitForPortFree(targetPort, 5000);
1945
2411
  if (!freed) {
1946
- console.log(chalk8.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
2412
+ console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
1947
2413
  }
1948
2414
  }
1949
2415
  }
1950
2416
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1951
2417
  env: spawnEnv,
1952
2418
  cwd: bgrDir2,
1953
- stdout: Bun.file(stdoutPath),
1954
- stderr: Bun.file(stderrPath)
2419
+ stdout: "ignore",
2420
+ stderr: "ignore",
2421
+ detached: true
1955
2422
  });
1956
2423
  newProcess.unref();
1957
2424
  await sleep3(2000);
1958
- const actualPid = await findChildPid(newProcess.pid);
2425
+ const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2426
+ const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
1959
2427
  let actualPort = null;
1960
2428
  for (let attempt = 0;attempt < 10; attempt++) {
1961
2429
  const ports = await getProcessPorts(actualPid);
@@ -1978,19 +2446,19 @@ async function run2() {
1978
2446
  const portDisplay = actualPort ? String(actualPort) : "(detecting...)";
1979
2447
  const urlDisplay = actualPort ? `http://localhost:${actualPort}` : "http://localhost (port auto-assigned)";
1980
2448
  const msg = dedent`
1981
- ${chalk8.bold("\u26A1 BGR Dashboard launched")}
1982
- ${chalk8.gray("\u2500".repeat(40))}
2449
+ ${chalk7.bold("\u26A1 BGR Dashboard launched")}
2450
+ ${chalk7.gray("\u2500".repeat(40))}
1983
2451
 
1984
- \u{1f310} Open in browser: ${chalk8.cyan.underline(urlDisplay)}
2452
+ \u{1f310} Open in browser: ${chalk7.cyan.underline(urlDisplay)}
1985
2453
  \u{1f4ca} Manage all your processes from the web UI
1986
2454
  \u{1f504} Auto-refreshes every 3 seconds
1987
2455
 
1988
- ${chalk8.gray("\u2500".repeat(40))}
1989
- Process: ${chalk8.white(dashboardName)} | PID: ${chalk8.white(String(actualPid))} | Port: ${chalk8.white(portDisplay)}
2456
+ ${chalk7.gray("\u2500".repeat(40))}
2457
+ Process: ${chalk7.white(dashboardName)} | PID: ${chalk7.white(String(actualPid))} | Port: ${chalk7.white(portDisplay)}
1990
2458
 
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
2459
+ ${chalk7.yellow("bgrun bgr-dashboard --logs")} View dashboard logs
2460
+ ${chalk7.yellow("bgrun --stop bgr-dashboard")} Stop the dashboard
2461
+ ${chalk7.yellow("bgrun --restart bgr-dashboard")} Restart the dashboard
1994
2462
  `;
1995
2463
  announce(msg, "BGR Dashboard");
1996
2464
  return;
@@ -1998,13 +2466,13 @@ async function run2() {
1998
2466
  if (values.guard) {
1999
2467
  const guardName = "bgr-guard";
2000
2468
  const homePath3 = getHomeDir();
2001
- const bgrDir2 = join3(homePath3, ".bgr");
2469
+ const bgrDir2 = join4(homePath3, ".bgr");
2002
2470
  const existing = getProcess(guardName);
2003
2471
  if (existing && await isProcessRunning(existing.pid)) {
2004
2472
  announce(`Guard is already running (PID ${existing.pid})
2005
2473
 
2006
- Use ${chalk8.yellow(`bgrun --stop ${guardName}`)} to stop it
2007
- Use ${chalk8.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2474
+ Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
2475
+ Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2008
2476
  return;
2009
2477
  }
2010
2478
  if (existing) {
@@ -2017,19 +2485,27 @@ async function run2() {
2017
2485
  const scriptPath = resolve(process.argv[1]);
2018
2486
  const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
2019
2487
  const command = `bgrun --_guard-loop`;
2020
- const stdoutPath = join3(bgrDir2, `${guardName}-out.txt`);
2021
- const stderrPath = join3(bgrDir2, `${guardName}-err.txt`);
2488
+ const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
2489
+ const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
2022
2490
  await Bun.write(stdoutPath, "");
2023
2491
  await Bun.write(stderrPath, "");
2024
2492
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
2025
- env: { ...Bun.env },
2493
+ env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
2026
2494
  cwd: bgrDir2,
2027
- stdout: Bun.file(stdoutPath),
2028
- stderr: Bun.file(stderrPath)
2495
+ stdout: "ignore",
2496
+ stderr: "ignore",
2497
+ detached: true
2029
2498
  });
2030
2499
  newProcess.unref();
2031
2500
  await sleep3(1000);
2032
- const actualPid = await findChildPid(newProcess.pid);
2501
+ let actualPid = await findChildPid(newProcess.pid);
2502
+ if (!await isProcessRunning(actualPid)) {
2503
+ const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
2504
+ 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);
2505
+ const foundPid = parseInt(result.trim());
2506
+ if (!isNaN(foundPid) && foundPid > 0)
2507
+ actualPid = foundPid;
2508
+ }
2033
2509
  await retryDatabaseOperation(() => insertProcess({
2034
2510
  pid: actualPid,
2035
2511
  workdir: bgrDir2,
@@ -2041,19 +2517,19 @@ async function run2() {
2041
2517
  stderr_path: stderrPath
2042
2518
  }));
2043
2519
  const msg = dedent`
2044
- ${chalk8.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2045
- ${chalk8.gray("\u2500".repeat(40))}
2520
+ ${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2521
+ ${chalk7.gray("\u2500".repeat(40))}
2046
2522
 
2047
2523
  Monitors: All processes with BGR_KEEP_ALIVE=true
2048
2524
  Also watches: bgr-dashboard (auto-restart if it dies)
2049
2525
  Check interval: 30 seconds
2050
2526
  Backoff: Exponential after 5 rapid crashes
2051
2527
 
2052
- ${chalk8.gray("\u2500".repeat(40))}
2053
- Process: ${chalk8.white(guardName)} | PID: ${chalk8.white(String(actualPid))}
2528
+ ${chalk7.gray("\u2500".repeat(40))}
2529
+ Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
2054
2530
 
2055
- ${chalk8.yellow(`bgrun ${guardName} --logs`)} View guard logs
2056
- ${chalk8.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2531
+ ${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
2532
+ ${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2057
2533
  `;
2058
2534
  announce(msg, "BGR Guard");
2059
2535
  return;
@@ -2070,13 +2546,13 @@ async function run2() {
2070
2546
  const info = getDbInfo();
2071
2547
  const version = await getVersion();
2072
2548
  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)}
2549
+ ${chalk7.bold("bgrun debug info")}
2550
+ ${chalk7.gray("\u2500".repeat(40))}
2551
+ Version: ${chalk7.cyan(version)}
2552
+ BGR Home: ${chalk7.yellow(info.bgrHome)}
2553
+ DB Path: ${chalk7.yellow(info.dbPath)}
2078
2554
  DB File: ${info.dbFilename}
2079
- DB Exists: ${info.exists ? chalk8.green("\u2713") : chalk8.red("\u2717")}
2555
+ DB Exists: ${info.exists ? chalk7.green("\u2713") : chalk7.red("\u2717")}
2080
2556
  Platform: ${process.platform}
2081
2557
  Bun: ${Bun.version}
2082
2558
  `);
@@ -2097,12 +2573,12 @@ async function run2() {
2097
2573
  error("No processes registered.");
2098
2574
  return;
2099
2575
  }
2100
- console.log(chalk8.bold(`
2576
+ console.log(chalk7.bold(`
2101
2577
  Restarting ${all.length} processes...
2102
2578
  `));
2103
2579
  for (const proc of all) {
2104
2580
  try {
2105
- console.log(chalk8.yellow(` \u21BB Restarting ${proc.name}...`));
2581
+ console.log(chalk7.yellow(` \u21BB Restarting ${proc.name}...`));
2106
2582
  await handleRun({
2107
2583
  action: "run",
2108
2584
  name: proc.name,
@@ -2110,10 +2586,10 @@ async function run2() {
2110
2586
  remoteName: ""
2111
2587
  });
2112
2588
  } catch (err) {
2113
- console.error(chalk8.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
2589
+ console.error(chalk7.red(` \u2717 Failed to restart ${proc.name}: ${err.message}`));
2114
2590
  }
2115
2591
  }
2116
- console.log(chalk8.green(`
2592
+ console.log(chalk7.green(`
2117
2593
  \u2713 All processes restarted.
2118
2594
  `));
2119
2595
  return;
@@ -2125,22 +2601,22 @@ async function run2() {
2125
2601
  error("No processes registered.");
2126
2602
  return;
2127
2603
  }
2128
- console.log(chalk8.bold(`
2604
+ console.log(chalk7.bold(`
2129
2605
  Stopping ${all.length} processes...
2130
2606
  `));
2131
2607
  for (const proc of all) {
2132
2608
  try {
2133
2609
  if (await isProcessRunning(proc.pid)) {
2134
- console.log(chalk8.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
2610
+ console.log(chalk7.yellow(` \u25A0 Stopping ${proc.name} (PID ${proc.pid})...`));
2135
2611
  await handleStop(proc.name);
2136
2612
  } else {
2137
- console.log(chalk8.gray(` \u25CB ${proc.name} already stopped`));
2613
+ console.log(chalk7.gray(` \u25CB ${proc.name} already stopped`));
2138
2614
  }
2139
2615
  } catch (err) {
2140
- console.error(chalk8.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
2616
+ console.error(chalk7.red(` \u2717 Failed to stop ${proc.name}: ${err.message}`));
2141
2617
  }
2142
2618
  }
2143
- console.log(chalk8.green(`
2619
+ console.log(chalk7.green(`
2144
2620
  \u2713 All processes stopped.
2145
2621
  `));
2146
2622
  return;
@@ -2230,5 +2706,8 @@ async function run2() {
2230
2706
  }
2231
2707
  }
2232
2708
  run2().catch((err) => {
2233
- error(err);
2709
+ if (err.name !== "BgrunError") {
2710
+ console.error(err);
2711
+ }
2712
+ process.exit(1);
2234
2713
  });