bgrun 3.12.23 → 3.12.25

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/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
  }
@@ -825,7 +825,7 @@ var init_db = __esm(() => {
825
825
  // src/utils.ts
826
826
  import * as fs2 from "fs";
827
827
  import * as os2 from "os";
828
- import { join as join3 } from "path";
828
+ import { delimiter, dirname, join as join3 } from "path";
829
829
  function parseEnvString(envString) {
830
830
  const env = {};
831
831
  envString.split(",").forEach((pair) => {
@@ -841,6 +841,17 @@ function calculateRuntime(startTime) {
841
841
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
842
842
  return `${diffInMinutes} minutes`;
843
843
  }
844
+ function prependPathEntry(existingPath, entry) {
845
+ if (!existingPath)
846
+ return entry;
847
+ const parts = existingPath.split(delimiter).filter(Boolean);
848
+ const normalizedEntry = process.platform === "win32" ? entry.toLowerCase() : entry;
849
+ const deduped = parts.filter((part) => {
850
+ const normalizedPart = process.platform === "win32" ? part.toLowerCase() : part;
851
+ return normalizedPart !== normalizedEntry;
852
+ });
853
+ return [entry, ...deduped].join(delimiter);
854
+ }
844
855
  function parseCommandEnv(command) {
845
856
  const env = {};
846
857
  const trimmed = command.trim();
@@ -882,6 +893,8 @@ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
882
893
  continue;
883
894
  sanitizedParentEnv[key] = value;
884
895
  }
896
+ const bunDir = dirname(process.execPath);
897
+ sanitizedParentEnv.PATH = prependPathEntry(sanitizedParentEnv.PATH, bunDir);
885
898
  return { ...sanitizedParentEnv, ...processEnv };
886
899
  }
887
900
  function stringifyEnvString(env) {
@@ -1181,79 +1194,28 @@ import { join as join4 } from "path";
1181
1194
 
1182
1195
  // src/cli-helpers.ts
1183
1196
  init_db();
1184
- var MONTH_NAMES = [
1185
- "january",
1186
- "february",
1187
- "march",
1188
- "april",
1189
- "may",
1190
- "june",
1191
- "july",
1192
- "august",
1193
- "september",
1194
- "october",
1195
- "november",
1196
- "december"
1197
- ];
1198
- var DAY_NAMES = [
1199
- "",
1200
- "first",
1201
- "second",
1202
- "third",
1203
- "fourth",
1204
- "fifth",
1205
- "sixth",
1206
- "seventh",
1207
- "eighth",
1208
- "ninth",
1209
- "tenth",
1210
- "eleventh",
1211
- "twelfth",
1212
- "thirteenth",
1213
- "fourteenth",
1214
- "fifteenth",
1215
- "sixteenth",
1216
- "seventeenth",
1217
- "eighteenth",
1218
- "nineteenth",
1219
- "twentieth",
1220
- "twenty-first",
1221
- "twenty-second",
1222
- "twenty-third",
1223
- "twenty-fourth",
1224
- "twenty-fifth",
1225
- "twenty-sixth",
1226
- "twenty-seventh",
1227
- "twenty-eighth",
1228
- "twenty-ninth",
1229
- "thirtieth",
1230
- "thirty-first"
1231
- ];
1232
- function pad2(value) {
1233
- return String(value).padStart(2, "0");
1234
- }
1235
- function buildDateProcessName(now = new Date) {
1236
- const month = MONTH_NAMES[now.getMonth()] || "process";
1237
- const day = DAY_NAMES[now.getDate()] || "day";
1238
- return `${month}-${day}`;
1239
- }
1240
- function generateAutoProcessName(now = new Date) {
1241
- const baseName = buildDateProcessName(now);
1197
+ import { basename, resolve } from "path";
1198
+ function sanitizeProcessName(value) {
1199
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "");
1200
+ return normalized || "process";
1201
+ }
1202
+ function buildDirectoryProcessName(directory) {
1203
+ const resolved = resolve(directory);
1204
+ const folderName = basename(resolved);
1205
+ return sanitizeProcessName(folderName);
1206
+ }
1207
+ function generateAutoProcessName(directory) {
1208
+ const baseName = buildDirectoryProcessName(directory);
1242
1209
  if (!getProcess(baseName)) {
1243
1210
  return baseName;
1244
1211
  }
1245
- const timeSuffix = `${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
1246
- const timeName = `${baseName}-${timeSuffix}`;
1247
- if (!getProcess(timeName)) {
1248
- return timeName;
1249
- }
1250
1212
  for (let i = 1;i < 1000; i++) {
1251
- const candidate = `${timeName}-${i}`;
1213
+ const candidate = `${baseName}-${i}`;
1252
1214
  if (!getProcess(candidate)) {
1253
1215
  return candidate;
1254
1216
  }
1255
1217
  }
1256
- return `${timeName}-${Date.now()}`;
1218
+ return `${baseName}-${Date.now()}`;
1257
1219
  }
1258
1220
  function shellQuoteArg(arg) {
1259
1221
  if (process.platform === "win32") {
@@ -1465,6 +1427,78 @@ function getRecentGuardEvents(limit = 100) {
1465
1427
  var homePath2 = getHomeDir();
1466
1428
  var run = createMeasure2("run");
1467
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
+ }
1468
1502
  function resolveInternalBgrunCommand(command) {
1469
1503
  const trimmed = command.trim();
1470
1504
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1675,11 +1709,13 @@ async function handleRun(options) {
1675
1709
  stderr: Bun.file(stderrPath)
1676
1710
  });
1677
1711
  newProcess.unref();
1678
- await sleep2(100);
1679
- const pid = await findChildPid(newProcess.pid);
1680
- await sleep2(400);
1681
- return pid;
1712
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1682
1713
  }) ?? 0;
1714
+ if (actualPid <= 0) {
1715
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1716
+ stdout: ${stdoutPath}
1717
+ stderr: ${stderrPath}`);
1718
+ }
1683
1719
  await retryDatabaseOperation(() => insertProcess({
1684
1720
  pid: actualPid,
1685
1721
  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
  }
@@ -825,7 +825,7 @@ var init_db = __esm(() => {
825
825
  // src/utils.ts
826
826
  import * as fs2 from "fs";
827
827
  import * as os2 from "os";
828
- import { join as join3 } from "path";
828
+ import { delimiter, dirname, join as join3 } from "path";
829
829
  function parseEnvString(envString) {
830
830
  const env = {};
831
831
  envString.split(",").forEach((pair) => {
@@ -841,6 +841,17 @@ function calculateRuntime(startTime) {
841
841
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
842
842
  return `${diffInMinutes} minutes`;
843
843
  }
844
+ function prependPathEntry(existingPath, entry) {
845
+ if (!existingPath)
846
+ return entry;
847
+ const parts = existingPath.split(delimiter).filter(Boolean);
848
+ const normalizedEntry = process.platform === "win32" ? entry.toLowerCase() : entry;
849
+ const deduped = parts.filter((part) => {
850
+ const normalizedPart = process.platform === "win32" ? part.toLowerCase() : part;
851
+ return normalizedPart !== normalizedEntry;
852
+ });
853
+ return [entry, ...deduped].join(delimiter);
854
+ }
844
855
  function parseCommandEnv(command) {
845
856
  const env = {};
846
857
  const trimmed = command.trim();
@@ -882,6 +893,8 @@ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
882
893
  continue;
883
894
  sanitizedParentEnv[key] = value;
884
895
  }
896
+ const bunDir = dirname(process.execPath);
897
+ sanitizedParentEnv.PATH = prependPathEntry(sanitizedParentEnv.PATH, bunDir);
885
898
  return { ...sanitizedParentEnv, ...processEnv };
886
899
  }
887
900
  function stringifyEnvString(env) {
@@ -1180,79 +1193,28 @@ import { join as join4 } from "path";
1180
1193
 
1181
1194
  // src/cli-helpers.ts
1182
1195
  init_db();
1183
- var MONTH_NAMES = [
1184
- "january",
1185
- "february",
1186
- "march",
1187
- "april",
1188
- "may",
1189
- "june",
1190
- "july",
1191
- "august",
1192
- "september",
1193
- "october",
1194
- "november",
1195
- "december"
1196
- ];
1197
- var DAY_NAMES = [
1198
- "",
1199
- "first",
1200
- "second",
1201
- "third",
1202
- "fourth",
1203
- "fifth",
1204
- "sixth",
1205
- "seventh",
1206
- "eighth",
1207
- "ninth",
1208
- "tenth",
1209
- "eleventh",
1210
- "twelfth",
1211
- "thirteenth",
1212
- "fourteenth",
1213
- "fifteenth",
1214
- "sixteenth",
1215
- "seventeenth",
1216
- "eighteenth",
1217
- "nineteenth",
1218
- "twentieth",
1219
- "twenty-first",
1220
- "twenty-second",
1221
- "twenty-third",
1222
- "twenty-fourth",
1223
- "twenty-fifth",
1224
- "twenty-sixth",
1225
- "twenty-seventh",
1226
- "twenty-eighth",
1227
- "twenty-ninth",
1228
- "thirtieth",
1229
- "thirty-first"
1230
- ];
1231
- function pad2(value) {
1232
- return String(value).padStart(2, "0");
1233
- }
1234
- function buildDateProcessName(now = new Date) {
1235
- const month = MONTH_NAMES[now.getMonth()] || "process";
1236
- const day = DAY_NAMES[now.getDate()] || "day";
1237
- return `${month}-${day}`;
1238
- }
1239
- function generateAutoProcessName(now = new Date) {
1240
- const baseName = buildDateProcessName(now);
1196
+ import { basename, resolve } from "path";
1197
+ function sanitizeProcessName(value) {
1198
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "");
1199
+ return normalized || "process";
1200
+ }
1201
+ function buildDirectoryProcessName(directory) {
1202
+ const resolved = resolve(directory);
1203
+ const folderName = basename(resolved);
1204
+ return sanitizeProcessName(folderName);
1205
+ }
1206
+ function generateAutoProcessName(directory) {
1207
+ const baseName = buildDirectoryProcessName(directory);
1241
1208
  if (!getProcess(baseName)) {
1242
1209
  return baseName;
1243
1210
  }
1244
- const timeSuffix = `${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
1245
- const timeName = `${baseName}-${timeSuffix}`;
1246
- if (!getProcess(timeName)) {
1247
- return timeName;
1248
- }
1249
1211
  for (let i = 1;i < 1000; i++) {
1250
- const candidate = `${timeName}-${i}`;
1212
+ const candidate = `${baseName}-${i}`;
1251
1213
  if (!getProcess(candidate)) {
1252
1214
  return candidate;
1253
1215
  }
1254
1216
  }
1255
- return `${timeName}-${Date.now()}`;
1217
+ return `${baseName}-${Date.now()}`;
1256
1218
  }
1257
1219
  function shellQuoteArg(arg) {
1258
1220
  if (process.platform === "win32") {
@@ -1464,6 +1426,78 @@ function getRecentGuardEvents(limit = 100) {
1464
1426
  var homePath2 = getHomeDir();
1465
1427
  var run = createMeasure2("run");
1466
1428
  var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1429
+ async function resolveSpawnedProcessPid(parentPid, command, workdir) {
1430
+ if (parentPid <= 0)
1431
+ return 0;
1432
+ let candidatePid = parentPid;
1433
+ const spawnedAt = Date.now();
1434
+ for (let attempt = 0;attempt < 6; attempt++) {
1435
+ const descendantPid = await findChildPid(parentPid);
1436
+ if (descendantPid > 0) {
1437
+ candidatePid = descendantPid;
1438
+ }
1439
+ if (candidatePid > 0 && await isProcessRunning(candidatePid, command)) {
1440
+ return candidatePid;
1441
+ }
1442
+ await sleep2(250);
1443
+ }
1444
+ const reconciled = await reconcileProcessPids([{ name: "__spawn__", pid: parentPid, command, workdir }], new Set([parentPid]));
1445
+ const matchedPid = reconciled.get("__spawn__") ?? 0;
1446
+ if (matchedPid > 0 && await isProcessRunning(matchedPid, command)) {
1447
+ return matchedPid;
1448
+ }
1449
+ if (process.platform === "win32") {
1450
+ try {
1451
+ const commandParts = command.toLowerCase().split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 2);
1452
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ` + `ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.CreationDate)|$($_.CommandLine)" }`, 5000);
1453
+ let bestPid = 0;
1454
+ let bestScore = -1;
1455
+ for (const line of output.split(`
1456
+ `)) {
1457
+ const parts = line.split("|");
1458
+ if (parts.length < 4)
1459
+ continue;
1460
+ const pid = parseInt(parts[0]?.trim(), 10);
1461
+ const candidateParentPid = parseInt(parts[1]?.trim(), 10);
1462
+ const creationDateRaw = parts[2]?.trim() || "";
1463
+ const candidateCommand = parts.slice(3).join("|").trim().toLowerCase();
1464
+ if (isNaN(pid) || pid <= 0 || pid === process.pid)
1465
+ continue;
1466
+ if (!candidateCommand)
1467
+ continue;
1468
+ let score = 0;
1469
+ if (candidateParentPid === parentPid)
1470
+ score += 10;
1471
+ for (const part of commandParts) {
1472
+ if (candidateCommand.includes(part))
1473
+ score += 2;
1474
+ }
1475
+ if (candidateCommand.includes("run server.ts"))
1476
+ score += 2;
1477
+ if (candidateCommand.includes(workdir.toLowerCase().replace(/\\/g, "/")))
1478
+ score += 4;
1479
+ if (candidateCommand.includes(workdir.toLowerCase()))
1480
+ score += 4;
1481
+ const createdAt = creationDateRaw ? Date.parse(creationDateRaw) : NaN;
1482
+ if (!isNaN(createdAt)) {
1483
+ const ageMs = Math.abs(createdAt - spawnedAt);
1484
+ if (ageMs <= 15000)
1485
+ score += 6;
1486
+ else if (ageMs <= 60000)
1487
+ score += 2;
1488
+ }
1489
+ if (score > bestScore && await isProcessRunning(pid, command)) {
1490
+ bestScore = score;
1491
+ bestPid = pid;
1492
+ }
1493
+ }
1494
+ if (bestScore >= 4) {
1495
+ return bestPid;
1496
+ }
1497
+ } catch {}
1498
+ }
1499
+ return 0;
1500
+ }
1467
1501
  function resolveInternalBgrunCommand(command) {
1468
1502
  const trimmed = command.trim();
1469
1503
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1674,11 +1708,13 @@ async function handleRun(options) {
1674
1708
  stderr: Bun.file(stderrPath)
1675
1709
  });
1676
1710
  newProcess.unref();
1677
- await sleep2(100);
1678
- const pid = await findChildPid(newProcess.pid);
1679
- await sleep2(400);
1680
- return pid;
1711
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1681
1712
  }) ?? 0;
1713
+ if (actualPid <= 0) {
1714
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1715
+ stdout: ${stdoutPath}
1716
+ stderr: ${stderrPath}`);
1717
+ }
1682
1718
  await retryDatabaseOperation(() => insertProcess({
1683
1719
  pid: actualPid,
1684
1720
  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
  }
@@ -825,7 +825,7 @@ var init_db = __esm(() => {
825
825
  // src/utils.ts
826
826
  import * as fs2 from "fs";
827
827
  import * as os2 from "os";
828
- import { join as join3 } from "path";
828
+ import { delimiter, dirname, join as join3 } from "path";
829
829
  function parseEnvString(envString) {
830
830
  const env = {};
831
831
  envString.split(",").forEach((pair) => {
@@ -841,6 +841,17 @@ function calculateRuntime(startTime) {
841
841
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
842
842
  return `${diffInMinutes} minutes`;
843
843
  }
844
+ function prependPathEntry(existingPath, entry) {
845
+ if (!existingPath)
846
+ return entry;
847
+ const parts = existingPath.split(delimiter).filter(Boolean);
848
+ const normalizedEntry = process.platform === "win32" ? entry.toLowerCase() : entry;
849
+ const deduped = parts.filter((part) => {
850
+ const normalizedPart = process.platform === "win32" ? part.toLowerCase() : part;
851
+ return normalizedPart !== normalizedEntry;
852
+ });
853
+ return [entry, ...deduped].join(delimiter);
854
+ }
844
855
  function parseCommandEnv(command) {
845
856
  const env = {};
846
857
  const trimmed = command.trim();
@@ -882,6 +893,8 @@ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
882
893
  continue;
883
894
  sanitizedParentEnv[key] = value;
884
895
  }
896
+ const bunDir = dirname(process.execPath);
897
+ sanitizedParentEnv.PATH = prependPathEntry(sanitizedParentEnv.PATH, bunDir);
885
898
  return { ...sanitizedParentEnv, ...processEnv };
886
899
  }
887
900
  function stringifyEnvString(env) {
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
  }
@@ -826,7 +826,7 @@ var init_db = __esm(() => {
826
826
  // src/utils.ts
827
827
  import * as fs2 from "fs";
828
828
  import * as os2 from "os";
829
- import { join as join3 } from "path";
829
+ import { delimiter, dirname, join as join3 } from "path";
830
830
  function parseEnvString(envString) {
831
831
  const env = {};
832
832
  envString.split(",").forEach((pair) => {
@@ -842,6 +842,17 @@ function calculateRuntime(startTime) {
842
842
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
843
843
  return `${diffInMinutes} minutes`;
844
844
  }
845
+ function prependPathEntry(existingPath, entry) {
846
+ if (!existingPath)
847
+ return entry;
848
+ const parts = existingPath.split(delimiter).filter(Boolean);
849
+ const normalizedEntry = process.platform === "win32" ? entry.toLowerCase() : entry;
850
+ const deduped = parts.filter((part) => {
851
+ const normalizedPart = process.platform === "win32" ? part.toLowerCase() : part;
852
+ return normalizedPart !== normalizedEntry;
853
+ });
854
+ return [entry, ...deduped].join(delimiter);
855
+ }
845
856
  function parseCommandEnv(command) {
846
857
  const env = {};
847
858
  const trimmed = command.trim();
@@ -883,6 +894,8 @@ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
883
894
  continue;
884
895
  sanitizedParentEnv[key] = value;
885
896
  }
897
+ const bunDir = dirname(process.execPath);
898
+ sanitizedParentEnv.PATH = prependPathEntry(sanitizedParentEnv.PATH, bunDir);
886
899
  return { ...sanitizedParentEnv, ...processEnv };
887
900
  }
888
901
  function stringifyEnvString(env) {
@@ -1341,79 +1354,28 @@ import { join as join4 } from "path";
1341
1354
 
1342
1355
  // src/cli-helpers.ts
1343
1356
  init_db();
1344
- var MONTH_NAMES = [
1345
- "january",
1346
- "february",
1347
- "march",
1348
- "april",
1349
- "may",
1350
- "june",
1351
- "july",
1352
- "august",
1353
- "september",
1354
- "october",
1355
- "november",
1356
- "december"
1357
- ];
1358
- var DAY_NAMES = [
1359
- "",
1360
- "first",
1361
- "second",
1362
- "third",
1363
- "fourth",
1364
- "fifth",
1365
- "sixth",
1366
- "seventh",
1367
- "eighth",
1368
- "ninth",
1369
- "tenth",
1370
- "eleventh",
1371
- "twelfth",
1372
- "thirteenth",
1373
- "fourteenth",
1374
- "fifteenth",
1375
- "sixteenth",
1376
- "seventeenth",
1377
- "eighteenth",
1378
- "nineteenth",
1379
- "twentieth",
1380
- "twenty-first",
1381
- "twenty-second",
1382
- "twenty-third",
1383
- "twenty-fourth",
1384
- "twenty-fifth",
1385
- "twenty-sixth",
1386
- "twenty-seventh",
1387
- "twenty-eighth",
1388
- "twenty-ninth",
1389
- "thirtieth",
1390
- "thirty-first"
1391
- ];
1392
- function pad2(value) {
1393
- return String(value).padStart(2, "0");
1394
- }
1395
- function buildDateProcessName(now = new Date) {
1396
- const month = MONTH_NAMES[now.getMonth()] || "process";
1397
- const day = DAY_NAMES[now.getDate()] || "day";
1398
- return `${month}-${day}`;
1399
- }
1400
- function generateAutoProcessName(now = new Date) {
1401
- const baseName = buildDateProcessName(now);
1357
+ import { basename, resolve } from "path";
1358
+ function sanitizeProcessName(value) {
1359
+ const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-._]+|[-._]+$/g, "");
1360
+ return normalized || "process";
1361
+ }
1362
+ function buildDirectoryProcessName(directory) {
1363
+ const resolved = resolve(directory);
1364
+ const folderName = basename(resolved);
1365
+ return sanitizeProcessName(folderName);
1366
+ }
1367
+ function generateAutoProcessName(directory) {
1368
+ const baseName = buildDirectoryProcessName(directory);
1402
1369
  if (!getProcess(baseName)) {
1403
1370
  return baseName;
1404
1371
  }
1405
- const timeSuffix = `${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
1406
- const timeName = `${baseName}-${timeSuffix}`;
1407
- if (!getProcess(timeName)) {
1408
- return timeName;
1409
- }
1410
1372
  for (let i = 1;i < 1000; i++) {
1411
- const candidate = `${timeName}-${i}`;
1373
+ const candidate = `${baseName}-${i}`;
1412
1374
  if (!getProcess(candidate)) {
1413
1375
  return candidate;
1414
1376
  }
1415
1377
  }
1416
- return `${timeName}-${Date.now()}`;
1378
+ return `${baseName}-${Date.now()}`;
1417
1379
  }
1418
1380
  function shellQuoteArg(arg) {
1419
1381
  if (process.platform === "win32") {
@@ -1625,6 +1587,78 @@ function getRecentGuardEvents(limit = 100) {
1625
1587
  var homePath2 = getHomeDir();
1626
1588
  var run = createMeasure2("run");
1627
1589
  var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1590
+ async function resolveSpawnedProcessPid(parentPid, command, workdir) {
1591
+ if (parentPid <= 0)
1592
+ return 0;
1593
+ let candidatePid = parentPid;
1594
+ const spawnedAt = Date.now();
1595
+ for (let attempt = 0;attempt < 6; attempt++) {
1596
+ const descendantPid = await findChildPid(parentPid);
1597
+ if (descendantPid > 0) {
1598
+ candidatePid = descendantPid;
1599
+ }
1600
+ if (candidatePid > 0 && await isProcessRunning(candidatePid, command)) {
1601
+ return candidatePid;
1602
+ }
1603
+ await sleep2(250);
1604
+ }
1605
+ const reconciled = await reconcileProcessPids([{ name: "__spawn__", pid: parentPid, command, workdir }], new Set([parentPid]));
1606
+ const matchedPid = reconciled.get("__spawn__") ?? 0;
1607
+ if (matchedPid > 0 && await isProcessRunning(matchedPid, command)) {
1608
+ return matchedPid;
1609
+ }
1610
+ if (process.platform === "win32") {
1611
+ try {
1612
+ const commandParts = command.toLowerCase().split(/\s+/).map((part) => part.trim()).filter((part) => part.length > 2);
1613
+ const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ` + `ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.CreationDate)|$($_.CommandLine)" }`, 5000);
1614
+ let bestPid = 0;
1615
+ let bestScore = -1;
1616
+ for (const line of output.split(`
1617
+ `)) {
1618
+ const parts = line.split("|");
1619
+ if (parts.length < 4)
1620
+ continue;
1621
+ const pid = parseInt(parts[0]?.trim(), 10);
1622
+ const candidateParentPid = parseInt(parts[1]?.trim(), 10);
1623
+ const creationDateRaw = parts[2]?.trim() || "";
1624
+ const candidateCommand = parts.slice(3).join("|").trim().toLowerCase();
1625
+ if (isNaN(pid) || pid <= 0 || pid === process.pid)
1626
+ continue;
1627
+ if (!candidateCommand)
1628
+ continue;
1629
+ let score = 0;
1630
+ if (candidateParentPid === parentPid)
1631
+ score += 10;
1632
+ for (const part of commandParts) {
1633
+ if (candidateCommand.includes(part))
1634
+ score += 2;
1635
+ }
1636
+ if (candidateCommand.includes("run server.ts"))
1637
+ score += 2;
1638
+ if (candidateCommand.includes(workdir.toLowerCase().replace(/\\/g, "/")))
1639
+ score += 4;
1640
+ if (candidateCommand.includes(workdir.toLowerCase()))
1641
+ score += 4;
1642
+ const createdAt = creationDateRaw ? Date.parse(creationDateRaw) : NaN;
1643
+ if (!isNaN(createdAt)) {
1644
+ const ageMs = Math.abs(createdAt - spawnedAt);
1645
+ if (ageMs <= 15000)
1646
+ score += 6;
1647
+ else if (ageMs <= 60000)
1648
+ score += 2;
1649
+ }
1650
+ if (score > bestScore && await isProcessRunning(pid, command)) {
1651
+ bestScore = score;
1652
+ bestPid = pid;
1653
+ }
1654
+ }
1655
+ if (bestScore >= 4) {
1656
+ return bestPid;
1657
+ }
1658
+ } catch {}
1659
+ }
1660
+ return 0;
1661
+ }
1628
1662
  function resolveInternalBgrunCommand(command) {
1629
1663
  const trimmed = command.trim();
1630
1664
  if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
@@ -1835,11 +1869,13 @@ async function handleRun(options) {
1835
1869
  stderr: Bun.file(stderrPath)
1836
1870
  });
1837
1871
  newProcess.unref();
1838
- await sleep2(100);
1839
- const pid = await findChildPid(newProcess.pid);
1840
- await sleep2(400);
1841
- return pid;
1872
+ return await resolveSpawnedProcessPid(newProcess.pid, finalCommand, finalDirectory);
1842
1873
  }) ?? 0;
1874
+ if (actualPid <= 0) {
1875
+ throw new Error(`Failed to resolve a live PID for "${name}" after launch. The child process likely exited immediately. Check logs:
1876
+ stdout: ${stdoutPath}
1877
+ stderr: ${stderrPath}`);
1878
+ }
1843
1879
  await retryDatabaseOperation(() => insertProcess({
1844
1880
  pid: actualPid,
1845
1881
  workdir: finalDirectory,
@@ -2039,7 +2075,18 @@ function formatMemory(bytes) {
2039
2075
  }
2040
2076
  async function showAll(opts) {
2041
2077
  const processes = getAllProcesses();
2042
- const filtered = processes.filter((proc) => {
2078
+ const latestByName = new Map;
2079
+ for (const proc of processes) {
2080
+ const existing = latestByName.get(proc.name);
2081
+ if (!existing) {
2082
+ latestByName.set(proc.name, proc);
2083
+ continue;
2084
+ }
2085
+ if (proc.timestamp > existing.timestamp || proc.timestamp === existing.timestamp && proc.id > existing.id) {
2086
+ latestByName.set(proc.name, proc);
2087
+ }
2088
+ }
2089
+ const filtered = Array.from(latestByName.values()).filter((proc) => {
2043
2090
  if (isInternalProcessName(proc.name))
2044
2091
  return false;
2045
2092
  if (!opts?.filter)
@@ -2071,9 +2118,19 @@ async function showAll(opts) {
2071
2118
  for (const proc of filtered) {
2072
2119
  const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
2073
2120
  const envVars = parseEnvString(proc.env);
2074
- const ports = isRunning ? await getProcessPorts(proc.pid) : [];
2121
+ let displayPid = proc.pid;
2122
+ let ports = [];
2123
+ if (isRunning) {
2124
+ const resolved = await resolvePidWithPorts(proc.pid);
2125
+ displayPid = resolved.pid;
2126
+ ports = resolved.ports;
2127
+ if (displayPid !== proc.pid) {
2128
+ updateProcessPid(proc.name, displayPid);
2129
+ proc.pid = displayPid;
2130
+ }
2131
+ }
2075
2132
  jsonData.push({
2076
- pid: proc.pid,
2133
+ pid: displayPid,
2077
2134
  name: proc.name,
2078
2135
  ports: ports.length > 0 ? ports : undefined,
2079
2136
  status: isRunning ? "running" : "stopped",
@@ -2334,7 +2391,7 @@ ${color(content)}
2334
2391
  return true;
2335
2392
  };
2336
2393
  const waitForLogReady = (logPath, timeoutMs = 5000) => {
2337
- return new Promise((resolve, reject) => {
2394
+ return new Promise((resolve2, reject) => {
2338
2395
  const checkReady = () => {
2339
2396
  try {
2340
2397
  if (fs4.existsSync(logPath)) {
@@ -2347,7 +2404,7 @@ ${color(content)}
2347
2404
  return false;
2348
2405
  };
2349
2406
  if (checkReady()) {
2350
- resolve();
2407
+ resolve2();
2351
2408
  return;
2352
2409
  }
2353
2410
  const dir = path2.dirname(logPath);
@@ -2356,7 +2413,7 @@ ${color(content)}
2356
2413
  if (changedFilename === filename && eventType === "change") {
2357
2414
  if (checkReady()) {
2358
2415
  watcher2.close();
2359
- resolve();
2416
+ resolve2();
2360
2417
  }
2361
2418
  }
2362
2419
  });
@@ -2874,7 +2931,7 @@ async function showHelp() {
2874
2931
  ${chalk7.yellow("Commands:")}
2875
2932
  bunx bgrun List all processes
2876
2933
  bunx bgrun [name] Show details for a process
2877
- bunx bgrun -- <cmd> Start a managed process with an auto-generated name
2934
+ bunx bgrun -- <cmd> Start a managed process named from the working directory
2878
2935
  bunx bgrun inline -- <cmd> Run a command in this terminal with config env loaded
2879
2936
  bunx bgrun --env Print shell commands to export config env vars
2880
2937
  bunx bgrun --dashboard Launch web dashboard (managed by bgrun)
@@ -2887,6 +2944,7 @@ async function showHelp() {
2887
2944
  bunx bgrun --delete [name] Delete a process
2888
2945
  bunx bgrun --clean Remove all stopped processes
2889
2946
  bunx bgrun --nuke Delete ALL processes
2947
+ bunx bgrun --kill-port <n> Kill whatever is currently listening on a port
2890
2948
 
2891
2949
  ${chalk7.yellow("Options:")}
2892
2950
  --name <string> Process name (required for new)
@@ -2897,6 +2955,7 @@ async function showHelp() {
2897
2955
  --env Print shell export commands from config and exit
2898
2956
  --shell <type> Shell for --env: powershell | cmd | sh | json
2899
2957
  --watch Watch for file changes and auto-restart
2958
+ --hot Restart the managed process when files change
2900
2959
  --force Force restart existing process
2901
2960
  --fetch Fetch latest git changes before running
2902
2961
  --json Output in JSON format
@@ -2910,17 +2969,21 @@ async function showHelp() {
2910
2969
  --dashboard Launch web dashboard as bgrun-managed process
2911
2970
  --guard Enable per-process crash watcher
2912
2971
  --guard-off Disable per-process crash watcher
2972
+ --kill-port <number> Kill the process currently using a port
2913
2973
  --port <number> Port for dashboard (default: 3000)
2914
2974
  --help Show this help message
2915
2975
 
2916
2976
  ${chalk7.yellow("Examples:")}
2917
2977
  bunx bgrun -- bun run dev
2978
+ bunx bgrun --hot -- bun run index.ts
2979
+ bunx bgrun -hl -- bun run server.ts
2918
2980
  bunx bgrun --no-config -- bun run script.ts
2919
2981
  bunx bgrun --force -- bun run server.ts
2920
2982
  bunx bgrun inline -- bun run dev
2921
2983
  Invoke-Expression (bunx bgrun --env)
2922
2984
  eval "$(bunx bgrun --env --shell sh)"
2923
2985
  bunx bgrun --dashboard
2986
+ bunx bgrun --kill-port 3000
2924
2987
  bunx bgrun myapp --guard
2925
2988
  bunx bgrun myapp --guard-off
2926
2989
  bunx bgrun --name myapp --command "bun run dev" --directory . --watch
@@ -2929,15 +2992,16 @@ async function showHelp() {
2929
2992
  console.log(usage);
2930
2993
  }
2931
2994
  var cliArgOptions = {
2932
- name: { type: "string" },
2933
- command: { type: "string" },
2934
- directory: { type: "string" },
2995
+ name: { type: "string", short: "n" },
2996
+ command: { type: "string", short: "c" },
2997
+ directory: { type: "string", short: "d" },
2935
2998
  config: { type: "string" },
2936
2999
  "no-config": { type: "boolean" },
2937
3000
  env: { type: "boolean" },
2938
3001
  shell: { type: "string" },
2939
- watch: { type: "boolean" },
2940
- force: { type: "boolean" },
3002
+ watch: { type: "boolean", short: "w" },
3003
+ hot: { type: "boolean", short: "h" },
3004
+ force: { type: "boolean", short: "f" },
2941
3005
  fetch: { type: "boolean" },
2942
3006
  delete: { type: "boolean" },
2943
3007
  nuke: { type: "boolean" },
@@ -2946,13 +3010,13 @@ var cliArgOptions = {
2946
3010
  stop: { type: "boolean" },
2947
3011
  "stop-all": { type: "boolean" },
2948
3012
  clean: { type: "boolean" },
2949
- json: { type: "boolean" },
2950
- logs: { type: "boolean" },
3013
+ json: { type: "boolean", short: "j" },
3014
+ logs: { type: "boolean", short: "l" },
2951
3015
  "log-stdout": { type: "boolean" },
2952
3016
  "log-stderr": { type: "boolean" },
2953
3017
  lines: { type: "string" },
2954
3018
  filter: { type: "string" },
2955
- version: { type: "boolean" },
3019
+ version: { type: "boolean", short: "v" },
2956
3020
  help: { type: "boolean" },
2957
3021
  db: { type: "string" },
2958
3022
  stdout: { type: "string" },
@@ -2961,6 +3025,7 @@ var cliArgOptions = {
2961
3025
  guard: { type: "boolean" },
2962
3026
  "guard-off": { type: "boolean" },
2963
3027
  debug: { type: "boolean" },
3028
+ "kill-port": { type: "string" },
2964
3029
  _serve: { type: "boolean" },
2965
3030
  "_watch-process": { type: "string" },
2966
3031
  port: { type: "string" }
@@ -2968,7 +3033,7 @@ var cliArgOptions = {
2968
3033
  async function run2() {
2969
3034
  const rawArgs = Bun.argv.slice(2);
2970
3035
  const isActionInvocation = (values2) => {
2971
- 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.json || values2.filter);
3036
+ 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);
2972
3037
  };
2973
3038
  if (rawArgs[0] === "inline") {
2974
3039
  const parsed = parseInlineArgs(rawArgs.slice(1));
@@ -3001,11 +3066,12 @@ async function run2() {
3001
3066
  strict: false,
3002
3067
  allowPositionals: true
3003
3068
  });
3004
- const autoName = values2.name || generateAutoProcessName();
3005
3069
  const inlineCommand = joinCommandArgs(commandArgs);
3006
3070
  const directory = values2.directory || process.cwd();
3007
- await handleRun({
3008
- action: "run",
3071
+ const autoName = values2.name || generateAutoProcessName(directory);
3072
+ const watchLike = Boolean(values2.watch || values2.hot);
3073
+ const runOptions = {
3074
+ action: watchLike ? "watch" : "run",
3009
3075
  name: autoName,
3010
3076
  command: inlineCommand,
3011
3077
  directory,
@@ -3016,7 +3082,16 @@ async function run2() {
3016
3082
  dbPath: values2.db,
3017
3083
  stdout: values2.stdout,
3018
3084
  stderr: values2.stderr
3019
- });
3085
+ };
3086
+ if (watchLike) {
3087
+ await handleWatch(runOptions, {
3088
+ showLogs: values2.logs || false,
3089
+ logType: values2["log-stdout"] ? "stdout" : values2["log-stderr"] ? "stderr" : "both",
3090
+ lines: values2.lines ? parseInt(values2.lines) : undefined
3091
+ });
3092
+ } else {
3093
+ await handleRun(runOptions);
3094
+ }
3020
3095
  return;
3021
3096
  }
3022
3097
  const { values, positionals } = parseArgs({
@@ -3037,11 +3112,12 @@ async function run2() {
3037
3112
  return;
3038
3113
  }
3039
3114
  if (positionals.length > 1 && !values.command && !isActionInvocation(values)) {
3040
- const autoName = values.name || generateAutoProcessName();
3041
3115
  const implicitCommand = joinCommandArgs(positionals);
3042
3116
  const directory = values.directory || process.cwd();
3043
- await handleRun({
3044
- action: "run",
3117
+ const autoName = values.name || generateAutoProcessName(directory);
3118
+ const watchLike = Boolean(values.watch || values.hot);
3119
+ const runOptions = {
3120
+ action: watchLike ? "watch" : "run",
3045
3121
  name: autoName,
3046
3122
  command: implicitCommand,
3047
3123
  directory,
@@ -3052,7 +3128,16 @@ async function run2() {
3052
3128
  dbPath: values.db,
3053
3129
  stdout: values.stdout,
3054
3130
  stderr: values.stderr
3055
- });
3131
+ };
3132
+ if (watchLike) {
3133
+ await handleWatch(runOptions, {
3134
+ showLogs: values.logs || false,
3135
+ logType: values["log-stdout"] ? "stdout" : values["log-stderr"] ? "stderr" : "both",
3136
+ lines: values.lines ? parseInt(values.lines) : undefined
3137
+ });
3138
+ } else {
3139
+ await handleRun(runOptions);
3140
+ }
3056
3141
  return;
3057
3142
  }
3058
3143
  if (values["_serve"]) {
@@ -3221,6 +3306,24 @@ async function run2() {
3221
3306
  `);
3222
3307
  return;
3223
3308
  }
3309
+ if (values["kill-port"]) {
3310
+ const port = parseInt(String(values["kill-port"]), 10);
3311
+ if (isNaN(port) || port <= 0) {
3312
+ error("Please provide a valid port number for --kill-port.");
3313
+ }
3314
+ const wasFree = await isPortFree(port);
3315
+ if (wasFree) {
3316
+ announce(`Port ${port} is already free.`, "Port Free");
3317
+ return;
3318
+ }
3319
+ await killProcessOnPort(port);
3320
+ const freed = await waitForPortFree(port, 5000);
3321
+ if (!freed) {
3322
+ error(`Port ${port} is still busy after attempted cleanup.`);
3323
+ }
3324
+ announce(`Freed port ${port}.`, "Port Cleanup");
3325
+ return;
3326
+ }
3224
3327
  if (values.nuke) {
3225
3328
  await handleDeleteAll();
3226
3329
  return;
@@ -3321,7 +3424,7 @@ async function run2() {
3321
3424
  await showLogs(name, logType, lines);
3322
3425
  return;
3323
3426
  }
3324
- if (values.watch) {
3427
+ if (values.watch || values.hot) {
3325
3428
  await handleWatch({
3326
3429
  action: "watch",
3327
3430
  name,
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.23",
3
+ "version": "3.12.25",
4
4
  "description": "bgrun — A lightweight process manager for Bun",
5
5
  "type": "module",
6
6
  "main": "./dist/api.js",