bgrun 3.12.24 → 3.12.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -719,16 +719,24 @@ Not directly — bgrun manages processes on the local machine. For remote manage
719
719
 
720
720
  ---
721
721
 
722
- ## Custom Log Paths
723
-
724
- By default, logs go to `~/.bgr/<name>-out.txt` and `~/.bgr/<name>-err.txt`. Override with:
722
+ ## Custom Log Paths
723
+
724
+ By default, logs go to `~/.bgr/<name>-out.txt` and `~/.bgr/<name>-err.txt`. Override with:
725
725
 
726
726
  ```bash
727
- bgrun --name api \
728
- --command "bun run server.ts" \
729
- --stdout /var/log/api/stdout.log \
730
- --stderr /var/log/api/stderr.log
731
- ```
727
+ bgrun --name api \
728
+ --command "bun run server.ts" \
729
+ --stdout /var/log/api/stdout.log \
730
+ --stderr /var/log/api/stderr.log
731
+ ```
732
+
733
+ Or provide a directory and let `bgrun` derive both log filenames from the process name:
734
+
735
+ ```bash
736
+ bgrun --name api \
737
+ --command "bun run server.ts" \
738
+ --logs-dir /var/log/api
739
+ ```
732
740
 
733
741
  ---
734
742
 
package/dist/api.js CHANGED
@@ -240,7 +240,7 @@ function ensureDir(dirPath) {
240
240
  }
241
241
  function getShellCommand(command) {
242
242
  if (isWindows()) {
243
- return ["cmd", "/c", command];
243
+ return [process.env.ComSpec || "cmd.exe", "/c", command];
244
244
  } else {
245
245
  return ["sh", "-c", command];
246
246
  }
@@ -1184,7 +1184,8 @@ async function loadConfigEnv(directory, configPath = ".config.toml") {
1184
1184
  // src/commands/run.ts
1185
1185
  var {$: $2 } = globalThis.Bun;
1186
1186
  var {sleep: sleep2 } = globalThis.Bun;
1187
- import { join as join5 } from "path";
1187
+ import { mkdirSync as mkdirSync3 } from "fs";
1188
+ import { dirname as dirname2, join as join5 } from "path";
1188
1189
  import { createMeasure as createMeasure2 } from "measure-fn";
1189
1190
 
1190
1191
  // src/watcher.ts
@@ -1427,6 +1428,78 @@ function getRecentGuardEvents(limit = 100) {
1427
1428
  var homePath2 = getHomeDir();
1428
1429
  var run = createMeasure2("run");
1429
1430
  var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1431
+ async function resolveSpawnedProcessPid(parentPid, command, workdir) {
1432
+ if (parentPid <= 0)
1433
+ return 0;
1434
+ let candidatePid = parentPid;
1435
+ const spawnedAt = Date.now();
1436
+ for (let attempt = 0;attempt < 6; attempt++) {
1437
+ const descendantPid = await findChildPid(parentPid);
1438
+ if (descendantPid > 0) {
1439
+ candidatePid = descendantPid;
1440
+ }
1441
+ if (candidatePid > 0 && await isProcessRunning(candidatePid, command)) {
1442
+ return candidatePid;
1443
+ }
1444
+ await sleep2(250);
1445
+ }
1446
+ const reconciled = await reconcileProcessPids([{ name: "__spawn__", pid: parentPid, command, workdir }], new Set([parentPid]));
1447
+ const matchedPid = reconciled.get("__spawn__") ?? 0;
1448
+ if (matchedPid > 0 && await isProcessRunning(matchedPid, command)) {
1449
+ return matchedPid;
1450
+ }
1451
+ if (process.platform === "win32") {
1452
+ try {
1453
+ const commandParts = command.toLowerCase().split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 2);
1454
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ` + `ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.CreationDate)|$($_.CommandLine)" }`, 5000);
1455
+ let bestPid = 0;
1456
+ let bestScore = -1;
1457
+ for (const line of output.split(`
1458
+ `)) {
1459
+ const parts = line.split("|");
1460
+ if (parts.length < 4)
1461
+ continue;
1462
+ const pid = parseInt(parts[0]?.trim(), 10);
1463
+ const candidateParentPid = parseInt(parts[1]?.trim(), 10);
1464
+ const creationDateRaw = parts[2]?.trim() || "";
1465
+ const candidateCommand = parts.slice(3).join("|").trim().toLowerCase();
1466
+ if (isNaN(pid) || pid <= 0 || pid === process.pid)
1467
+ continue;
1468
+ if (!candidateCommand)
1469
+ continue;
1470
+ let score = 0;
1471
+ if (candidateParentPid === parentPid)
1472
+ score += 10;
1473
+ for (const part of commandParts) {
1474
+ if (candidateCommand.includes(part))
1475
+ score += 2;
1476
+ }
1477
+ if (candidateCommand.includes("run server.ts"))
1478
+ score += 2;
1479
+ if (candidateCommand.includes(workdir.toLowerCase().replace(/\\/g, "/")))
1480
+ score += 4;
1481
+ if (candidateCommand.includes(workdir.toLowerCase()))
1482
+ score += 4;
1483
+ const createdAt = creationDateRaw ? Date.parse(creationDateRaw) : NaN;
1484
+ if (!isNaN(createdAt)) {
1485
+ const ageMs = Math.abs(createdAt - spawnedAt);
1486
+ if (ageMs <= 15000)
1487
+ score += 6;
1488
+ else if (ageMs <= 60000)
1489
+ score += 2;
1490
+ }
1491
+ if (score > bestScore && await isProcessRunning(pid, command)) {
1492
+ bestScore = score;
1493
+ bestPid = pid;
1494
+ }
1495
+ }
1496
+ if (bestScore >= 4) {
1497
+ return bestPid;
1498
+ }
1499
+ } catch {}
1500
+ }
1501
+ return 0;
1502
+ }
1430
1503
  function resolveInternalBgrunCommand(command) {
1431
1504
  const trimmed = command.trim();
1432
1505
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1446,6 +1519,7 @@ async function handleRun(options) {
1446
1519
  configPath,
1447
1520
  force,
1448
1521
  fetch,
1522
+ logsDir,
1449
1523
  stdout,
1450
1524
  stderr
1451
1525
  } = options;
@@ -1625,9 +1699,11 @@ async function handleRun(options) {
1625
1699
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1626
1700
  }
1627
1701
  }
1628
- const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1702
+ const stdoutPath = stdout || (logsDir ? join5(logsDir, `${name}-out.txt`) : undefined) || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1703
+ mkdirSync3(dirname2(stdoutPath), { recursive: true });
1629
1704
  Bun.write(stdoutPath, "");
1630
- const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1705
+ const stderrPath = stderr || (logsDir ? join5(logsDir, `${name}-err.txt`) : undefined) || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1706
+ mkdirSync3(dirname2(stderrPath), { recursive: true });
1631
1707
  Bun.write(stderrPath, "");
1632
1708
  const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1633
1709
  const newProcess = Bun.spawn(getShellCommand(finalCommand), {
@@ -1637,11 +1713,13 @@ async function handleRun(options) {
1637
1713
  stderr: Bun.file(stderrPath)
1638
1714
  });
1639
1715
  newProcess.unref();
1640
- await sleep2(100);
1641
- const pid = await findChildPid(newProcess.pid);
1642
- await sleep2(400);
1643
- return pid;
1716
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1644
1717
  }) ?? 0;
1718
+ if (actualPid <= 0) {
1719
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1720
+ stdout: ${stdoutPath}
1721
+ stderr: ${stderrPath}`);
1722
+ }
1645
1723
  await retryDatabaseOperation(() => insertProcess({
1646
1724
  pid: actualPid,
1647
1725
  workdir: finalDirectory,
package/dist/deploy.js CHANGED
@@ -240,7 +240,7 @@ function ensureDir(dirPath) {
240
240
  }
241
241
  function getShellCommand(command) {
242
242
  if (isWindows()) {
243
- return ["cmd", "/c", command];
243
+ return [process.env.ComSpec || "cmd.exe", "/c", command];
244
244
  } else {
245
245
  return ["sh", "-c", command];
246
246
  }
@@ -1183,7 +1183,8 @@ async function loadConfigEnv(directory, configPath = ".config.toml") {
1183
1183
  // src/commands/run.ts
1184
1184
  var {$: $2 } = globalThis.Bun;
1185
1185
  var {sleep: sleep2 } = globalThis.Bun;
1186
- import { join as join5 } from "path";
1186
+ import { mkdirSync as mkdirSync3 } from "fs";
1187
+ import { dirname as dirname2, join as join5 } from "path";
1187
1188
  import { createMeasure as createMeasure2 } from "measure-fn";
1188
1189
 
1189
1190
  // src/watcher.ts
@@ -1426,6 +1427,78 @@ function getRecentGuardEvents(limit = 100) {
1426
1427
  var homePath2 = getHomeDir();
1427
1428
  var run = createMeasure2("run");
1428
1429
  var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1430
+ async function resolveSpawnedProcessPid(parentPid, command, workdir) {
1431
+ if (parentPid <= 0)
1432
+ return 0;
1433
+ let candidatePid = parentPid;
1434
+ const spawnedAt = Date.now();
1435
+ for (let attempt = 0;attempt < 6; attempt++) {
1436
+ const descendantPid = await findChildPid(parentPid);
1437
+ if (descendantPid > 0) {
1438
+ candidatePid = descendantPid;
1439
+ }
1440
+ if (candidatePid > 0 && await isProcessRunning(candidatePid, command)) {
1441
+ return candidatePid;
1442
+ }
1443
+ await sleep2(250);
1444
+ }
1445
+ const reconciled = await reconcileProcessPids([{ name: "__spawn__", pid: parentPid, command, workdir }], new Set([parentPid]));
1446
+ const matchedPid = reconciled.get("__spawn__") ?? 0;
1447
+ if (matchedPid > 0 && await isProcessRunning(matchedPid, command)) {
1448
+ return matchedPid;
1449
+ }
1450
+ if (process.platform === "win32") {
1451
+ try {
1452
+ const commandParts = command.toLowerCase().split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 2);
1453
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ` + `ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.CreationDate)|$($_.CommandLine)" }`, 5000);
1454
+ let bestPid = 0;
1455
+ let bestScore = -1;
1456
+ for (const line of output.split(`
1457
+ `)) {
1458
+ const parts = line.split("|");
1459
+ if (parts.length < 4)
1460
+ continue;
1461
+ const pid = parseInt(parts[0]?.trim(), 10);
1462
+ const candidateParentPid = parseInt(parts[1]?.trim(), 10);
1463
+ const creationDateRaw = parts[2]?.trim() || "";
1464
+ const candidateCommand = parts.slice(3).join("|").trim().toLowerCase();
1465
+ if (isNaN(pid) || pid <= 0 || pid === process.pid)
1466
+ continue;
1467
+ if (!candidateCommand)
1468
+ continue;
1469
+ let score = 0;
1470
+ if (candidateParentPid === parentPid)
1471
+ score += 10;
1472
+ for (const part of commandParts) {
1473
+ if (candidateCommand.includes(part))
1474
+ score += 2;
1475
+ }
1476
+ if (candidateCommand.includes("run server.ts"))
1477
+ score += 2;
1478
+ if (candidateCommand.includes(workdir.toLowerCase().replace(/\\/g, "/")))
1479
+ score += 4;
1480
+ if (candidateCommand.includes(workdir.toLowerCase()))
1481
+ score += 4;
1482
+ const createdAt = creationDateRaw ? Date.parse(creationDateRaw) : NaN;
1483
+ if (!isNaN(createdAt)) {
1484
+ const ageMs = Math.abs(createdAt - spawnedAt);
1485
+ if (ageMs <= 15000)
1486
+ score += 6;
1487
+ else if (ageMs <= 60000)
1488
+ score += 2;
1489
+ }
1490
+ if (score > bestScore && await isProcessRunning(pid, command)) {
1491
+ bestScore = score;
1492
+ bestPid = pid;
1493
+ }
1494
+ }
1495
+ if (bestScore >= 4) {
1496
+ return bestPid;
1497
+ }
1498
+ } catch {}
1499
+ }
1500
+ return 0;
1501
+ }
1429
1502
  function resolveInternalBgrunCommand(command) {
1430
1503
  const trimmed = command.trim();
1431
1504
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1445,6 +1518,7 @@ async function handleRun(options) {
1445
1518
  configPath,
1446
1519
  force,
1447
1520
  fetch,
1521
+ logsDir,
1448
1522
  stdout,
1449
1523
  stderr
1450
1524
  } = options;
@@ -1624,9 +1698,11 @@ async function handleRun(options) {
1624
1698
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1625
1699
  }
1626
1700
  }
1627
- const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1701
+ const stdoutPath = stdout || (logsDir ? join5(logsDir, `${name}-out.txt`) : undefined) || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1702
+ mkdirSync3(dirname2(stdoutPath), { recursive: true });
1628
1703
  Bun.write(stdoutPath, "");
1629
- const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1704
+ const stderrPath = stderr || (logsDir ? join5(logsDir, `${name}-err.txt`) : undefined) || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1705
+ mkdirSync3(dirname2(stderrPath), { recursive: true });
1630
1706
  Bun.write(stderrPath, "");
1631
1707
  const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1632
1708
  const newProcess = Bun.spawn(getShellCommand(finalCommand), {
@@ -1636,11 +1712,13 @@ async function handleRun(options) {
1636
1712
  stderr: Bun.file(stderrPath)
1637
1713
  });
1638
1714
  newProcess.unref();
1639
- await sleep2(100);
1640
- const pid = await findChildPid(newProcess.pid);
1641
- await sleep2(400);
1642
- return pid;
1715
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1643
1716
  }) ?? 0;
1717
+ if (actualPid <= 0) {
1718
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1719
+ stdout: ${stdoutPath}
1720
+ stderr: ${stderrPath}`);
1721
+ }
1644
1722
  await retryDatabaseOperation(() => insertProcess({
1645
1723
  pid: actualPid,
1646
1724
  workdir: finalDirectory,
package/dist/deps.js CHANGED
@@ -240,7 +240,7 @@ function ensureDir(dirPath) {
240
240
  }
241
241
  function getShellCommand(command) {
242
242
  if (isWindows()) {
243
- return ["cmd", "/c", command];
243
+ return [process.env.ComSpec || "cmd.exe", "/c", command];
244
244
  } else {
245
245
  return ["sh", "-c", command];
246
246
  }
package/dist/index.js CHANGED
@@ -241,7 +241,7 @@ function ensureDir(dirPath) {
241
241
  }
242
242
  function getShellCommand(command) {
243
243
  if (isWindows()) {
244
- return ["cmd", "/c", command];
244
+ return [process.env.ComSpec || "cmd.exe", "/c", command];
245
245
  } else {
246
246
  return ["sh", "-c", command];
247
247
  }
@@ -1344,7 +1344,8 @@ async function loadConfigEnv(directory, configPath = ".config.toml") {
1344
1344
  // src/commands/run.ts
1345
1345
  var {$: $2 } = globalThis.Bun;
1346
1346
  var {sleep: sleep2 } = globalThis.Bun;
1347
- import { join as join5 } from "path";
1347
+ import { mkdirSync as mkdirSync3 } from "fs";
1348
+ import { dirname as dirname2, join as join5 } from "path";
1348
1349
  import { createMeasure as createMeasure2 } from "measure-fn";
1349
1350
 
1350
1351
  // src/watcher.ts
@@ -1587,6 +1588,78 @@ function getRecentGuardEvents(limit = 100) {
1587
1588
  var homePath2 = getHomeDir();
1588
1589
  var run = createMeasure2("run");
1589
1590
  var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1591
+ async function resolveSpawnedProcessPid(parentPid, command, workdir) {
1592
+ if (parentPid <= 0)
1593
+ return 0;
1594
+ let candidatePid = parentPid;
1595
+ const spawnedAt = Date.now();
1596
+ for (let attempt = 0;attempt < 6; attempt++) {
1597
+ const descendantPid = await findChildPid(parentPid);
1598
+ if (descendantPid > 0) {
1599
+ candidatePid = descendantPid;
1600
+ }
1601
+ if (candidatePid > 0 && await isProcessRunning(candidatePid, command)) {
1602
+ return candidatePid;
1603
+ }
1604
+ await sleep2(250);
1605
+ }
1606
+ const reconciled = await reconcileProcessPids([{ name: "__spawn__", pid: parentPid, command, workdir }], new Set([parentPid]));
1607
+ const matchedPid = reconciled.get("__spawn__") ?? 0;
1608
+ if (matchedPid > 0 && await isProcessRunning(matchedPid, command)) {
1609
+ return matchedPid;
1610
+ }
1611
+ if (process.platform === "win32") {
1612
+ try {
1613
+ const commandParts = command.toLowerCase().split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 2);
1614
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ` + `ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.CreationDate)|$($_.CommandLine)" }`, 5000);
1615
+ let bestPid = 0;
1616
+ let bestScore = -1;
1617
+ for (const line of output.split(`
1618
+ `)) {
1619
+ const parts = line.split("|");
1620
+ if (parts.length < 4)
1621
+ continue;
1622
+ const pid = parseInt(parts[0]?.trim(), 10);
1623
+ const candidateParentPid = parseInt(parts[1]?.trim(), 10);
1624
+ const creationDateRaw = parts[2]?.trim() || "";
1625
+ const candidateCommand = parts.slice(3).join("|").trim().toLowerCase();
1626
+ if (isNaN(pid) || pid <= 0 || pid === process.pid)
1627
+ continue;
1628
+ if (!candidateCommand)
1629
+ continue;
1630
+ let score = 0;
1631
+ if (candidateParentPid === parentPid)
1632
+ score += 10;
1633
+ for (const part of commandParts) {
1634
+ if (candidateCommand.includes(part))
1635
+ score += 2;
1636
+ }
1637
+ if (candidateCommand.includes("run server.ts"))
1638
+ score += 2;
1639
+ if (candidateCommand.includes(workdir.toLowerCase().replace(/\\/g, "/")))
1640
+ score += 4;
1641
+ if (candidateCommand.includes(workdir.toLowerCase()))
1642
+ score += 4;
1643
+ const createdAt = creationDateRaw ? Date.parse(creationDateRaw) : NaN;
1644
+ if (!isNaN(createdAt)) {
1645
+ const ageMs = Math.abs(createdAt - spawnedAt);
1646
+ if (ageMs <= 15000)
1647
+ score += 6;
1648
+ else if (ageMs <= 60000)
1649
+ score += 2;
1650
+ }
1651
+ if (score > bestScore && await isProcessRunning(pid, command)) {
1652
+ bestScore = score;
1653
+ bestPid = pid;
1654
+ }
1655
+ }
1656
+ if (bestScore >= 4) {
1657
+ return bestPid;
1658
+ }
1659
+ } catch {}
1660
+ }
1661
+ return 0;
1662
+ }
1590
1663
  function resolveInternalBgrunCommand(command) {
1591
1664
  const trimmed = command.trim();
1592
1665
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1606,6 +1679,7 @@ async function handleRun(options) {
1606
1679
  configPath,
1607
1680
  force,
1608
1681
  fetch,
1682
+ logsDir,
1609
1683
  stdout,
1610
1684
  stderr
1611
1685
  } = options;
@@ -1785,9 +1859,11 @@ async function handleRun(options) {
1785
1859
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1786
1860
  }
1787
1861
  }
1788
- const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1862
+ const stdoutPath = stdout || (logsDir ? join5(logsDir, `${name}-out.txt`) : undefined) || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1863
+ mkdirSync3(dirname2(stdoutPath), { recursive: true });
1789
1864
  Bun.write(stdoutPath, "");
1790
- const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1865
+ const stderrPath = stderr || (logsDir ? join5(logsDir, `${name}-err.txt`) : undefined) || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1866
+ mkdirSync3(dirname2(stderrPath), { recursive: true });
1791
1867
  Bun.write(stderrPath, "");
1792
1868
  const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1793
1869
  const newProcess = Bun.spawn(getShellCommand(finalCommand), {
@@ -1797,11 +1873,13 @@ async function handleRun(options) {
1797
1873
  stderr: Bun.file(stderrPath)
1798
1874
  });
1799
1875
  newProcess.unref();
1800
- await sleep2(100);
1801
- const pid = await findChildPid(newProcess.pid);
1802
- await sleep2(400);
1803
- return pid;
1876
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1804
1877
  }) ?? 0;
1878
+ if (actualPid <= 0) {
1879
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1880
+ stdout: ${stdoutPath}
1881
+ stderr: ${stderrPath}`);
1882
+ }
1805
1883
  await retryDatabaseOperation(() => insertProcess({
1806
1884
  pid: actualPid,
1807
1885
  workdir: finalDirectory,
@@ -2870,6 +2948,7 @@ async function showHelp() {
2870
2948
  bunx bgrun --delete [name] Delete a process
2871
2949
  bunx bgrun --clean Remove all stopped processes
2872
2950
  bunx bgrun --nuke Delete ALL processes
2951
+ bunx bgrun --kill-port <n> Kill whatever is currently listening on a port
2873
2952
 
2874
2953
  ${chalk7.yellow("Options:")}
2875
2954
  --name <string> Process name (required for new)
@@ -2882,6 +2961,7 @@ async function showHelp() {
2882
2961
  --watch Watch for file changes and auto-restart
2883
2962
  --hot Restart the managed process when files change
2884
2963
  --force Force restart existing process
2964
+ --logs-dir <path> Directory for derived stdout/stderr logs
2885
2965
  --fetch Fetch latest git changes before running
2886
2966
  --json Output in JSON format
2887
2967
  --filter <group> Filter list by BGR_GROUP
@@ -2894,36 +2974,41 @@ async function showHelp() {
2894
2974
  --dashboard Launch web dashboard as bgrun-managed process
2895
2975
  --guard Enable per-process crash watcher
2896
2976
  --guard-off Disable per-process crash watcher
2977
+ --kill-port <number> Kill the process currently using a port
2897
2978
  --port <number> Port for dashboard (default: 3000)
2898
2979
  --help Show this help message
2899
2980
 
2900
2981
  ${chalk7.yellow("Examples:")}
2901
2982
  bunx bgrun -- bun run dev
2902
2983
  bunx bgrun --hot -- bun run index.ts
2984
+ bunx bgrun -hl -- bun run server.ts
2903
2985
  bunx bgrun --no-config -- bun run script.ts
2904
2986
  bunx bgrun --force -- bun run server.ts
2905
2987
  bunx bgrun inline -- bun run dev
2906
2988
  Invoke-Expression (bunx bgrun --env)
2907
2989
  eval "$(bunx bgrun --env --shell sh)"
2908
2990
  bunx bgrun --dashboard
2991
+ bunx bgrun --kill-port 3000
2909
2992
  bunx bgrun myapp --guard
2910
2993
  bunx bgrun myapp --guard-off
2911
2994
  bunx bgrun --name myapp --command "bun run dev" --directory . --watch
2995
+ bunx bgrun --name myapp --logs-dir .data --command "bun run dev" --directory .
2912
2996
  bunx bgrun myapp --logs --lines 50
2913
2997
  `;
2914
2998
  console.log(usage);
2915
2999
  }
2916
3000
  var cliArgOptions = {
2917
- name: { type: "string" },
2918
- command: { type: "string" },
2919
- directory: { type: "string" },
3001
+ name: { type: "string", short: "n" },
3002
+ command: { type: "string", short: "c" },
3003
+ directory: { type: "string", short: "d" },
2920
3004
  config: { type: "string" },
2921
3005
  "no-config": { type: "boolean" },
2922
3006
  env: { type: "boolean" },
2923
3007
  shell: { type: "string" },
2924
- watch: { type: "boolean" },
2925
- hot: { type: "boolean" },
2926
- force: { type: "boolean" },
3008
+ watch: { type: "boolean", short: "w" },
3009
+ hot: { type: "boolean", short: "h" },
3010
+ force: { type: "boolean", short: "f" },
3011
+ "logs-dir": { type: "string" },
2927
3012
  fetch: { type: "boolean" },
2928
3013
  delete: { type: "boolean" },
2929
3014
  nuke: { type: "boolean" },
@@ -2932,13 +3017,13 @@ var cliArgOptions = {
2932
3017
  stop: { type: "boolean" },
2933
3018
  "stop-all": { type: "boolean" },
2934
3019
  clean: { type: "boolean" },
2935
- json: { type: "boolean" },
2936
- logs: { type: "boolean" },
3020
+ json: { type: "boolean", short: "j" },
3021
+ logs: { type: "boolean", short: "l" },
2937
3022
  "log-stdout": { type: "boolean" },
2938
3023
  "log-stderr": { type: "boolean" },
2939
3024
  lines: { type: "string" },
2940
3025
  filter: { type: "string" },
2941
- version: { type: "boolean" },
3026
+ version: { type: "boolean", short: "v" },
2942
3027
  help: { type: "boolean" },
2943
3028
  db: { type: "string" },
2944
3029
  stdout: { type: "string" },
@@ -2947,6 +3032,7 @@ var cliArgOptions = {
2947
3032
  guard: { type: "boolean" },
2948
3033
  "guard-off": { type: "boolean" },
2949
3034
  debug: { type: "boolean" },
3035
+ "kill-port": { type: "string" },
2950
3036
  _serve: { type: "boolean" },
2951
3037
  "_watch-process": { type: "string" },
2952
3038
  port: { type: "string" }
@@ -2954,7 +3040,7 @@ var cliArgOptions = {
2954
3040
  async function run2() {
2955
3041
  const rawArgs = Bun.argv.slice(2);
2956
3042
  const isActionInvocation = (values2) => {
2957
- return Boolean(values2.dashboard || values2.guard || values2["guard-off"] || values2.version || values2.help || values2.debug || values2.nuke || values2.clean || values2["restart-all"] || values2["stop-all"] || values2.delete || values2.restart || values2.stop || values2.logs || values2["log-stdout"] || values2["log-stderr"] || values2.watch || values2.hot || values2.json || values2.filter);
3043
+ return Boolean(values2.dashboard || values2.guard || values2["guard-off"] || values2.version || values2.help || values2.debug || values2["kill-port"] || values2.nuke || values2.clean || values2["restart-all"] || values2["stop-all"] || values2.delete || values2.restart || values2.stop || values2.logs || values2["log-stdout"] || values2["log-stderr"] || values2.watch || values2.hot || values2.json || values2.filter);
2958
3044
  };
2959
3045
  if (rawArgs[0] === "inline") {
2960
3046
  const parsed = parseInlineArgs(rawArgs.slice(1));
@@ -2999,6 +3085,7 @@ async function run2() {
2999
3085
  configPath: values2["no-config"] ? "" : values2.config,
3000
3086
  force: values2.force,
3001
3087
  fetch: values2.fetch,
3088
+ logsDir: values2["logs-dir"],
3002
3089
  remoteName: "",
3003
3090
  dbPath: values2.db,
3004
3091
  stdout: values2.stdout,
@@ -3045,6 +3132,7 @@ async function run2() {
3045
3132
  configPath: values["no-config"] ? "" : values.config,
3046
3133
  force: values.force,
3047
3134
  fetch: values.fetch,
3135
+ logsDir: values["logs-dir"],
3048
3136
  remoteName: "",
3049
3137
  dbPath: values.db,
3050
3138
  stdout: values.stdout,
@@ -3227,6 +3315,24 @@ async function run2() {
3227
3315
  `);
3228
3316
  return;
3229
3317
  }
3318
+ if (values["kill-port"]) {
3319
+ const port = parseInt(String(values["kill-port"]), 10);
3320
+ if (isNaN(port) || port <= 0) {
3321
+ error("Please provide a valid port number for --kill-port.");
3322
+ }
3323
+ const wasFree = await isPortFree(port);
3324
+ if (wasFree) {
3325
+ announce(`Port ${port} is already free.`, "Port Free");
3326
+ return;
3327
+ }
3328
+ await killProcessOnPort(port);
3329
+ const freed = await waitForPortFree(port, 5000);
3330
+ if (!freed) {
3331
+ error(`Port ${port} is still busy after attempted cleanup.`);
3332
+ }
3333
+ announce(`Freed port ${port}.`, "Port Cleanup");
3334
+ return;
3335
+ }
3230
3336
  if (values.nuke) {
3231
3337
  await handleDeleteAll();
3232
3338
  return;
@@ -3335,6 +3441,7 @@ async function run2() {
3335
3441
  directory: values.directory,
3336
3442
  configPath: values.config,
3337
3443
  force: values.force,
3444
+ logsDir: values["logs-dir"],
3338
3445
  remoteName: "",
3339
3446
  dbPath: values.db,
3340
3447
  stdout: values.stdout,
@@ -3365,6 +3472,7 @@ async function run2() {
3365
3472
  configPath: values["no-config"] ? "" : values.config,
3366
3473
  force: values.force,
3367
3474
  fetch: values.fetch,
3475
+ logsDir: values["logs-dir"],
3368
3476
  remoteName: "",
3369
3477
  dbPath: values.db,
3370
3478
  stdout: values.stdout,
package/dist/server.js CHANGED
@@ -311,7 +311,7 @@ function ensureDir(dirPath) {
311
311
  }
312
312
  function getShellCommand(command) {
313
313
  if (isWindows()) {
314
- return ["cmd", "/c", command];
314
+ return [process.env.ComSpec || "cmd.exe", "/c", command];
315
315
  } else {
316
316
  return ["sh", "-c", command];
317
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bgrun",
3
- "version": "3.12.24",
3
+ "version": "3.12.26",
4
4
  "description": "bgrun — A lightweight process manager for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/api.js",