bgrun 3.12.15 → 3.12.21

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
@@ -18,48 +18,59 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
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
- parseUnixListeningPorts: () => parseUnixListeningPorts,
29
- killProcessOnPort: () => killProcessOnPort,
30
- isWindows: () => isWindows,
31
- isProcessRunning: () => isProcessRunning,
32
- isPortFree: () => isPortFree,
33
- getShellCommand: () => getShellCommand,
34
- getProcessPorts: () => getProcessPorts,
35
- getProcessMemory: () => getProcessMemory,
36
- getProcessBatchResources: () => getProcessBatchResources,
37
- getPortInfo: () => getPortInfo,
38
- getHomeDir: () => getHomeDir,
39
- findPidByPort: () => findPidByPort,
40
- findChildPid: () => findChildPid,
41
- ensureDir: () => ensureDir,
42
- copyFile: () => copyFile
43
- });
44
21
  import * as fs from "fs";
45
22
  import * as os from "os";
46
23
  import { join } from "path";
47
24
  var {$ } = globalThis.Bun;
48
25
  import { createMeasure } from "measure-fn";
49
- function psExec(command, _timeoutMs = 3000) {
50
- const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
26
+ async function psExec(command, timeoutMs = 3000) {
27
+ const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.ps1`);
51
28
  try {
52
- fs.writeFileSync(tmpFile, command);
53
- const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
29
+ await Bun.write(tmpFile, command);
30
+ const proc = Bun.spawn([
31
+ "powershell",
32
+ "-NoProfile",
33
+ "-ExecutionPolicy",
34
+ "Bypass",
35
+ "-File",
36
+ tmpFile
37
+ ], {
38
+ stdout: "pipe",
39
+ stderr: "pipe"
40
+ });
41
+ const timeoutPromise = new Promise((_, reject) => {
42
+ setTimeout(() => reject(new Error("PowerShell command timed out")), timeoutMs);
43
+ });
44
+ const resultPromise = new Promise(async (resolve, reject) => {
45
+ try {
46
+ const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
47
+ const stderrPromise = proc.stderr ? new Response(proc.stderr).text() : Promise.resolve("");
48
+ const exitCode = await proc.exited;
49
+ const stdout = await stdoutPromise;
50
+ const stderr = await stderrPromise;
51
+ if (exitCode === 0) {
52
+ resolve(stdout);
53
+ } else {
54
+ resolve(stderr || "");
55
+ }
56
+ } catch (error) {
57
+ reject(error);
58
+ }
59
+ });
54
60
  try {
55
- fs.unlinkSync(tmpFile);
56
- } catch {}
57
- return result.stdout?.toString() || "";
58
- } catch {
61
+ const result = await Promise.race([resultPromise, timeoutPromise]);
62
+ return result.trim();
63
+ } catch (error) {
64
+ return "";
65
+ } finally {
66
+ try {
67
+ await Bun.sleep(100);
68
+ } catch {}
69
+ }
70
+ } finally {
59
71
  try {
60
- fs.unlinkSync(tmpFile);
72
+ fs.rmSync(tmpFile, { force: true });
61
73
  } catch {}
62
- return "";
63
74
  }
64
75
  }
65
76
  function isWindows() {
@@ -81,7 +92,7 @@ async function isProcessRunning(pid, command) {
81
92
  process.kill(pid, 0);
82
93
  return true;
83
94
  } catch {
84
- const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
95
+ const output = await psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`);
85
96
  return output === String(pid);
86
97
  }
87
98
  } else {
@@ -174,35 +185,6 @@ async function isPortFree(port) {
174
185
  return true;
175
186
  }
176
187
  }
177
- async function getPortInfo(port) {
178
- try {
179
- if (isWindows()) {
180
- const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
181
- for (const line of result.split(`
182
- `)) {
183
- const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
184
- if (match) {
185
- const pid = parseInt(match[2]);
186
- if (pid > 0 && await isProcessRunning(pid)) {
187
- const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
188
- return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
189
- }
190
- }
191
- }
192
- return { inUse: false };
193
- } else {
194
- const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
195
- const lines = result.trim().split(`
196
- `).filter((l) => l.trim());
197
- if (lines.length > 1) {
198
- return { inUse: true };
199
- }
200
- return { inUse: false };
201
- }
202
- } catch {
203
- return { inUse: false };
204
- }
205
- }
206
188
  async function waitForPortFree(port, timeoutMs = 5000) {
207
189
  const startTime = Date.now();
208
190
  const pollInterval = 300;
@@ -410,9 +392,6 @@ async function readFileTail(filePath, lines) {
410
392
  }
411
393
  }) ?? "";
412
394
  }
413
- function copyFile(src, dest) {
414
- fs.copyFileSync(src, dest);
415
- }
416
395
  async function getProcessMemory(pid) {
417
396
  const map = await getProcessBatchResources([pid]);
418
397
  return map.get(pid)?.memory || 0;
@@ -425,7 +404,7 @@ async function getProcessBatchResources(pids) {
425
404
  const pidSet = new Set(pids);
426
405
  try {
427
406
  if (isWindows()) {
428
- const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
407
+ const output = await psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
429
408
  for (const line of output.split(`
430
409
  `)) {
431
410
  const sepIdx = line.indexOf("|");
@@ -505,6 +484,21 @@ async function getProcessPorts(pid) {
505
484
  return [];
506
485
  }
507
486
  }
487
+ async function resolvePidWithPorts(pid) {
488
+ const ports = await getProcessPorts(pid);
489
+ if (ports.length > 0 || !isWindows() || pid <= 0) {
490
+ return { pid, ports };
491
+ }
492
+ const childPid = await findChildPid(pid);
493
+ if (childPid === pid || childPid <= 0) {
494
+ return { pid, ports };
495
+ }
496
+ const childPorts = await getProcessPorts(childPid);
497
+ if (childPorts.length > 0) {
498
+ return { pid: childPid, ports: childPorts };
499
+ }
500
+ return { pid, ports };
501
+ }
508
502
  var plat;
509
503
  var init_platform = __esm(() => {
510
504
  plat = createMeasure("platform");
@@ -581,7 +575,7 @@ function removeProcessByName(name) {
581
575
  function updateProcessPid(name, newPid) {
582
576
  const proc = db.process.select().where({ name }).limit(1).get();
583
577
  if (proc) {
584
- db.process.update(proc.id, { pid: newPid });
578
+ proc.update({ pid: newPid });
585
579
  }
586
580
  }
587
581
  function removeAllProcesses() {
@@ -593,7 +587,7 @@ function removeAllProcesses() {
593
587
  function updateProcessEnv(name, envJson) {
594
588
  const proc = db.process.select().where({ name }).limit(1).get();
595
589
  if (proc) {
596
- db.process.update(proc.id, { env: envJson });
590
+ proc.update({ env: envJson });
597
591
  }
598
592
  }
599
593
  function getAllTemplates() {
@@ -831,6 +825,8 @@ var init_db = __esm(() => {
831
825
 
832
826
  // src/utils.ts
833
827
  import * as fs2 from "fs";
828
+ import * as os2 from "os";
829
+ import { join as join3 } from "path";
834
830
  function parseEnvString(envString) {
835
831
  const env = {};
836
832
  envString.split(",").forEach((pair) => {
@@ -846,9 +842,92 @@ function calculateRuntime(startTime) {
846
842
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
847
843
  return `${diffInMinutes} minutes`;
848
844
  }
845
+ function parseCommandEnv(command) {
846
+ const env = {};
847
+ const trimmed = command.trim();
848
+ const windowsSegments = trimmed.split(/&&/).map((segment) => segment.trim());
849
+ for (const segment of windowsSegments) {
850
+ const match = segment.match(/^set\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/i);
851
+ if (!match)
852
+ break;
853
+ env[match[1]] = match[2].trim();
854
+ }
855
+ const unixPrefixRegex = /^(?:([A-Za-z_][A-Za-z0-9_]*)=([^\s]+)\s+)+/;
856
+ const unixPrefix = trimmed.match(unixPrefixRegex);
857
+ if (unixPrefix) {
858
+ const pairs = unixPrefix[0].trim().split(/\s+/);
859
+ for (const pair of pairs) {
860
+ const eqIdx = pair.indexOf("=");
861
+ if (eqIdx <= 0)
862
+ continue;
863
+ const key = pair.slice(0, eqIdx);
864
+ const value = pair.slice(eqIdx + 1);
865
+ if (key)
866
+ env[key] = value;
867
+ }
868
+ }
869
+ return env;
870
+ }
871
+ function getDeclaredPort(processEnv, command) {
872
+ const mergedEnv = { ...command ? parseCommandEnv(command) : {}, ...processEnv };
873
+ const raw = mergedEnv.PORT || mergedEnv.BUN_PORT || "";
874
+ const parsed = parseInt(raw, 10);
875
+ return !isNaN(parsed) && parsed > 0 ? parsed : null;
876
+ }
877
+ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
878
+ const sanitizedParentEnv = {};
879
+ for (const [key, value] of Object.entries(parentEnv)) {
880
+ if (value === undefined)
881
+ continue;
882
+ if (INTERNAL_MANAGED_ENV_KEYS.includes(key))
883
+ continue;
884
+ sanitizedParentEnv[key] = value;
885
+ }
886
+ return { ...sanitizedParentEnv, ...processEnv };
887
+ }
888
+ function stringifyEnvString(env) {
889
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`).join(",");
890
+ }
891
+ function getWatcherProcessName(targetName) {
892
+ return `${WATCHER_PREFIX}${encodeURIComponent(targetName)}`;
893
+ }
894
+ function getWatchedProcessName(watcherName) {
895
+ if (!watcherName.startsWith(WATCHER_PREFIX))
896
+ return null;
897
+ try {
898
+ return decodeURIComponent(watcherName.slice(WATCHER_PREFIX.length));
899
+ } catch {
900
+ return null;
901
+ }
902
+ }
903
+ function isWatcherProcessName(name) {
904
+ return getWatchedProcessName(name) !== null;
905
+ }
906
+ function isInternalProcessName(name) {
907
+ return name === "bgr-dashboard" || name === "bgr-guard" || isWatcherProcessName(name);
908
+ }
909
+ function getOperationLockPath(name) {
910
+ return join3(os2.homedir(), ".bgr", `${name}.operation.lock`);
911
+ }
912
+ function acquireProcessOperationLock(name) {
913
+ const lockPath = getOperationLockPath(name);
914
+ fs2.mkdirSync(join3(os2.homedir(), ".bgr"), { recursive: true });
915
+ fs2.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, time: Date.now() }));
916
+ let released = false;
917
+ return () => {
918
+ if (released)
919
+ return;
920
+ released = true;
921
+ try {
922
+ fs2.unlinkSync(lockPath);
923
+ } catch {}
924
+ };
925
+ }
926
+ function isProcessOperationLocked(name) {
927
+ return fs2.existsSync(getOperationLockPath(name));
928
+ }
849
929
  async function getVersion() {
850
930
  try {
851
- const { join: join3 } = await import("path");
852
931
  const pkgPath = join3(import.meta.dir, "../package.json");
853
932
  const pkg = await Bun.file(pkgPath).json();
854
933
  return pkg.version || "0.0.0";
@@ -904,8 +983,10 @@ function tailFile(path, prefix, colorFn, lines) {
904
983
  } catch {}
905
984
  };
906
985
  }
986
+ var INTERNAL_MANAGED_ENV_KEYS, WATCHER_PREFIX = "bgr-watch-";
907
987
  var init_utils = __esm(() => {
908
988
  init_platform();
989
+ INTERNAL_MANAGED_ENV_KEYS = ["BUN_PORT", "BGR_STDOUT", "BGR_STDERR"];
909
990
  });
910
991
 
911
992
  // src/deps.ts
@@ -1068,6 +1149,102 @@ var init_log_rotation = __esm(() => {
1068
1149
  DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
1069
1150
  });
1070
1151
 
1152
+ // src/server.ts
1153
+ var exports_server = {};
1154
+ __export(exports_server, {
1155
+ startServer: () => startServer,
1156
+ guardRestartCounts: () => guardRestartCounts,
1157
+ guardEvents: () => guardEvents
1158
+ });
1159
+ import path from "path";
1160
+ async function cleanupPort(port) {
1161
+ if (process.platform !== "win32")
1162
+ return port;
1163
+ try {
1164
+ const occupiedBefore = !await isPortFree(port);
1165
+ if (!occupiedBefore)
1166
+ return port;
1167
+ console.log(`[server] Reclaiming port ${port} before dashboard start`);
1168
+ await killProcessOnPort(port);
1169
+ const freed = await waitForPortFree(port, 8000);
1170
+ if (freed)
1171
+ return port;
1172
+ console.warn(`[server] Port ${port} still busy after first cleanup; retrying`);
1173
+ await killProcessOnPort(port);
1174
+ const freedAfterRetry = await waitForPortFree(port, 5000);
1175
+ if (freedAfterRetry)
1176
+ return port;
1177
+ const fallback = port + 1;
1178
+ console.warn(`[server] \u26A0 Could not reclaim port ${port}; falling back to port ${fallback}`);
1179
+ return fallback;
1180
+ } catch {
1181
+ return port;
1182
+ }
1183
+ }
1184
+ async function startServer() {
1185
+ const { start } = await import("melina");
1186
+ const appDir = path.join(import.meta.dir, "../dashboard/app");
1187
+ const rawRequestedPort = process.env.BUN_PORT?.trim();
1188
+ const explicitPort = rawRequestedPort ? parseInt(rawRequestedPort, 10) : null;
1189
+ const hasExplicitPort = explicitPort !== null && !isNaN(explicitPort) && explicitPort > 0;
1190
+ const requestedPort = hasExplicitPort ? explicitPort : 3000;
1191
+ _originalPort = requestedPort;
1192
+ const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
1193
+ _currentPort = resolvedPort;
1194
+ const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
1195
+ await start({
1196
+ appDir,
1197
+ defaultTitle: "bgrun Dashboard - Process Manager",
1198
+ globalCss: path.join(appDir, "globals.css"),
1199
+ ...needsExplicitPort && { port: resolvedPort }
1200
+ });
1201
+ const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1202
+ startLogRotation2(() => getAllProcesses());
1203
+ if (resolvedPort !== requestedPort) {
1204
+ startStickyPortChecker();
1205
+ }
1206
+ }
1207
+ function startStickyPortChecker() {
1208
+ const CHECK_INTERVAL_MS = 60000;
1209
+ console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
1210
+ setInterval(async () => {
1211
+ if (_currentPort === _originalPort)
1212
+ return;
1213
+ try {
1214
+ const proc = Bun.spawn([
1215
+ "powershell",
1216
+ "-NoProfile",
1217
+ "-Command",
1218
+ `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1219
+ ], { stdout: "pipe", stderr: "pipe" });
1220
+ const text = await new Response(proc.stdout).text();
1221
+ const pid = parseInt(text.trim(), 10);
1222
+ if (!pid) {
1223
+ console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
1224
+ _currentPort = _originalPort;
1225
+ }
1226
+ } catch {}
1227
+ }, CHECK_INTERVAL_MS);
1228
+ }
1229
+ var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
1230
+ var init_server = __esm(async () => {
1231
+ init_db();
1232
+ init_platform();
1233
+ guardRestartCounts = new Map;
1234
+ guardEvents = [];
1235
+ if (import.meta.main) {
1236
+ await startServer();
1237
+ }
1238
+ });
1239
+
1240
+ // src/index.ts
1241
+ init_utils();
1242
+ import { parseArgs } from "util";
1243
+
1244
+ // src/commands/run.ts
1245
+ init_db();
1246
+ init_platform();
1247
+
1071
1248
  // src/logger.ts
1072
1249
  import boxen from "boxen";
1073
1250
  import chalk from "chalk";
@@ -1081,6 +1258,13 @@ function announce(message, title) {
1081
1258
  borderStyle: "round"
1082
1259
  }));
1083
1260
  }
1261
+
1262
+ class BgrunError extends Error {
1263
+ constructor(message) {
1264
+ super(message);
1265
+ this.name = "BgrunError";
1266
+ }
1267
+ }
1084
1268
  function error(message) {
1085
1269
  const text = message instanceof Error ? message.stack || message.message : String(message);
1086
1270
  console.error(boxen(chalk.red(text), {
@@ -1093,15 +1277,9 @@ function error(message) {
1093
1277
  }));
1094
1278
  throw new BgrunError(text);
1095
1279
  }
1096
- var BgrunError;
1097
- var init_logger = __esm(() => {
1098
- BgrunError = class BgrunError extends Error {
1099
- constructor(message) {
1100
- super(message);
1101
- this.name = "BgrunError";
1102
- }
1103
- };
1104
- });
1280
+
1281
+ // src/commands/run.ts
1282
+ init_utils();
1105
1283
 
1106
1284
  // src/config.ts
1107
1285
  function formatEnvKey(key) {
@@ -1133,516 +1311,553 @@ async function parseConfigFile(configPath) {
1133
1311
  const parsedConfig = await import(importPath).then((m) => m.default);
1134
1312
  return flattenConfig(parsedConfig);
1135
1313
  }
1314
+ async function loadConfigEnv(directory, configPath = ".config.toml") {
1315
+ const { join: join4 } = await import("path");
1316
+ const fullConfigPath = join4(directory, configPath);
1317
+ if (!await Bun.file(fullConfigPath).exists()) {
1318
+ return {
1319
+ configEnv: {},
1320
+ fullConfigPath,
1321
+ exists: false
1322
+ };
1323
+ }
1324
+ return {
1325
+ configEnv: await parseConfigFile(fullConfigPath),
1326
+ fullConfigPath,
1327
+ exists: true
1328
+ };
1329
+ }
1136
1330
 
1137
1331
  // src/commands/run.ts
1138
1332
  var {$: $2 } = globalThis.Bun;
1139
1333
  var {sleep: sleep2 } = globalThis.Bun;
1140
- import { join as join3 } from "path";
1334
+ import { join as join5 } from "path";
1141
1335
  import { createMeasure as createMeasure2 } from "measure-fn";
1142
- async function handleRun(options) {
1143
- const { command, directory, env, name, configPath, force, fetch: fetch2, stdout, stderr } = options;
1144
- const existingProcess = name ? getProcess(name) : null;
1145
- if (name && existingProcess) {
1146
- const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1147
- const unmet = await getUnmetDeps2(name);
1148
- if (unmet.length > 0) {
1149
- await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1150
- for (const depName of unmet) {
1151
- const depProc = getProcess(depName);
1152
- if (depProc) {
1153
- announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1154
- await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
1155
- }
1156
- }
1157
- });
1158
- }
1336
+
1337
+ // src/watcher.ts
1338
+ init_db();
1339
+ init_platform();
1340
+ import { join as join4 } from "path";
1341
+
1342
+ // src/cli-helpers.ts
1343
+ 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);
1402
+ if (!getProcess(baseName)) {
1403
+ return baseName;
1404
+ }
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
+ for (let i = 1;i < 1000; i++) {
1411
+ const candidate = `${timeName}-${i}`;
1412
+ if (!getProcess(candidate)) {
1413
+ return candidate;
1414
+ }
1415
+ }
1416
+ return `${timeName}-${Date.now()}`;
1417
+ }
1418
+ function shellQuoteArg(arg) {
1419
+ if (process.platform === "win32") {
1420
+ if (/^[A-Za-z0-9_./:\\-]+$/.test(arg))
1421
+ return arg;
1422
+ return `"${arg.replace(/"/g, "\\\"")}"`;
1423
+ }
1424
+ if (/^[A-Za-z0-9_./:-]+$/.test(arg))
1425
+ return arg;
1426
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
1427
+ }
1428
+ function joinCommandArgs(args) {
1429
+ return args.map(shellQuoteArg).join(" ");
1430
+ }
1431
+
1432
+ // src/watcher.ts
1433
+ init_utils();
1434
+ var DEFAULT_INTERVAL_MS = 5000;
1435
+ var CRASH_THRESHOLD = 5;
1436
+ var MAX_BACKOFF_MS = 5 * 60000;
1437
+ var STABILITY_WINDOW_MS = 120000;
1438
+ function getWatcherLogPaths(watcherName) {
1439
+ const homePath2 = getHomeDir();
1440
+ return {
1441
+ stdoutPath: join4(homePath2, ".bgr", `${watcherName}-out.txt`),
1442
+ stderrPath: join4(homePath2, ".bgr", `${watcherName}-err.txt`)
1443
+ };
1444
+ }
1445
+ async function findDetachedWatcherPid(targetName) {
1446
+ if (process.platform !== "win32")
1447
+ return null;
1448
+ const escaped = targetName.replace(/'/g, "''");
1449
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -like '*--_watch-process*' -and $_.CommandLine -like '*${escaped}*' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 4000);
1450
+ const pid = parseInt(result.trim(), 10);
1451
+ return !isNaN(pid) && pid > 0 ? pid : null;
1452
+ }
1453
+ function getInternalWatcherCommand(targetName) {
1454
+ const quotedTarget = shellQuoteArg(targetName);
1455
+ return {
1456
+ storedCommand: `bunx bgrun --_watch-process ${quotedTarget}`,
1457
+ spawnCommand: `bunx bgrun --_watch-process ${quotedTarget}`
1458
+ };
1459
+ }
1460
+ async function ensureProcessWatcher(targetName) {
1461
+ if (!targetName || isInternalProcessName(targetName))
1462
+ return;
1463
+ const proc = getProcess(targetName);
1464
+ if (!proc)
1465
+ return;
1466
+ const env = parseEnvString(proc.env || "");
1467
+ if (env.BGR_KEEP_ALIVE !== "true") {
1468
+ await stopProcessWatcher(targetName);
1469
+ return;
1470
+ }
1471
+ const watcherName = getWatcherProcessName(targetName);
1472
+ const existingWatcher = getProcess(watcherName);
1473
+ if (existingWatcher && await isProcessRunning(existingWatcher.pid, existingWatcher.command)) {
1474
+ return;
1475
+ }
1476
+ if (existingWatcher) {
1477
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1478
+ }
1479
+ const { stdoutPath, stderrPath } = getWatcherLogPaths(watcherName);
1480
+ await Bun.write(stdoutPath, "");
1481
+ await Bun.write(stderrPath, "");
1482
+ const { storedCommand, spawnCommand } = getInternalWatcherCommand(targetName);
1483
+ const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1484
+ env: {
1485
+ ...Bun.env,
1486
+ BGR_STDOUT: stdoutPath,
1487
+ BGR_STDERR: stderrPath
1488
+ },
1489
+ cwd: getHomeDir(),
1490
+ stdout: "ignore",
1491
+ stderr: "ignore",
1492
+ detached: true
1493
+ });
1494
+ newProcess.unref();
1495
+ await Bun.sleep(1000);
1496
+ let actualPid = await findChildPid(newProcess.pid);
1497
+ if (!await isProcessRunning(actualPid)) {
1498
+ const detachedPid = await findDetachedWatcherPid(targetName);
1499
+ if (detachedPid)
1500
+ actualPid = detachedPid;
1159
1501
  }
1160
- if (existingProcess) {
1161
- const finalDirectory2 = directory || existingProcess.workdir;
1162
- validateDirectory(finalDirectory2);
1163
- $2.cwd(finalDirectory2);
1164
- if (fetch2) {
1165
- if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1166
- error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1502
+ await retryDatabaseOperation(() => insertProcess({
1503
+ pid: actualPid,
1504
+ workdir: getHomeDir(),
1505
+ command: storedCommand,
1506
+ name: watcherName,
1507
+ env: stringifyEnvString({ BGR_KEEP_ALIVE: "false", BGR_WATCH_TARGET: targetName }),
1508
+ configPath: "",
1509
+ stdout_path: stdoutPath,
1510
+ stderr_path: stderrPath
1511
+ }));
1512
+ }
1513
+ async function stopProcessWatcher(targetName) {
1514
+ const watcherName = getWatcherProcessName(targetName);
1515
+ const watcherProc = getProcess(watcherName);
1516
+ if (!watcherProc)
1517
+ return;
1518
+ if (await isProcessRunning(watcherProc.pid, watcherProc.command)) {
1519
+ await terminateProcess(watcherProc.pid, true);
1520
+ }
1521
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1522
+ }
1523
+ async function syncProcessWatcher(targetName, env) {
1524
+ if (!targetName || isInternalProcessName(targetName))
1525
+ return;
1526
+ if (env.BGR_KEEP_ALIVE === "true") {
1527
+ await ensureProcessWatcher(targetName);
1528
+ } else {
1529
+ await stopProcessWatcher(targetName);
1530
+ }
1531
+ }
1532
+ function getBackoffMs(restartCount) {
1533
+ if (restartCount <= CRASH_THRESHOLD)
1534
+ return 0;
1535
+ const exponent = restartCount - CRASH_THRESHOLD;
1536
+ return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
1537
+ }
1538
+ async function cleanupWatcher(targetName) {
1539
+ const watcherName = getWatcherProcessName(targetName);
1540
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1541
+ }
1542
+ async function startProcessWatcher(targetName, intervalMs = DEFAULT_INTERVAL_MS) {
1543
+ const watcherName = getWatcherProcessName(targetName);
1544
+ const releaseWatcherLock = acquireProcessOperationLock(watcherName);
1545
+ let restartCount = 0;
1546
+ let nextRestartAt = 0;
1547
+ let lastSeenAliveAt = 0;
1548
+ try {
1549
+ console.log(`[watcher] Watching "${targetName}" every ${Math.round(intervalMs / 1000)}s`);
1550
+ while (true) {
1551
+ const proc = getProcess(targetName);
1552
+ if (!proc) {
1553
+ console.log(`[watcher] Target "${targetName}" removed; exiting watcher`);
1554
+ break;
1555
+ }
1556
+ const env = parseEnvString(proc.env || "");
1557
+ if (env.BGR_KEEP_ALIVE !== "true") {
1558
+ console.log(`[watcher] Guard disabled for "${targetName}"; exiting watcher`);
1559
+ break;
1167
1560
  }
1168
- await run.measure(`Git fetch "${name}"`, async () => {
1561
+ if (proc.pid <= 0 || isProcessOperationLocked(targetName)) {
1562
+ await Bun.sleep(intervalMs);
1563
+ continue;
1564
+ }
1565
+ const alive = await isProcessRunning(proc.pid, proc.command);
1566
+ if (!alive) {
1567
+ const now = Date.now();
1568
+ if (now < nextRestartAt) {
1569
+ await Bun.sleep(intervalMs);
1570
+ continue;
1571
+ }
1169
1572
  try {
1170
- await $2`git fetch origin`;
1171
- const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1172
- const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1173
- if (localHash !== remoteHash) {
1174
- await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1175
- announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1176
- }
1573
+ console.log(`[watcher] Restarting "${targetName}" after detected crash`);
1574
+ await handleRun({
1575
+ action: "run",
1576
+ name: targetName,
1577
+ force: true,
1578
+ remoteName: ""
1579
+ });
1580
+ restartCount++;
1581
+ const backoffMs = getBackoffMs(restartCount);
1582
+ nextRestartAt = backoffMs > 0 ? now + backoffMs : 0;
1583
+ lastSeenAliveAt = 0;
1584
+ addHistoryEntry(targetName, "guard_restart", proc.pid, { by: watcherName, count: restartCount, backoffMs });
1177
1585
  } catch (err) {
1178
- error(`Failed to pull latest changes: ${err}`);
1586
+ addHistoryEntry(targetName, "guard_restart_failed", proc.pid, { by: watcherName, error: err?.message || String(err) });
1587
+ console.error(`[watcher] Failed to restart "${targetName}": ${err.message}`);
1179
1588
  }
1180
- });
1181
- }
1182
- const isRunning = await isProcessRunning(existingProcess.pid);
1183
- if (isRunning && !force) {
1184
- error(`Process '${name}' is currently running. Use --force to restart.`);
1185
- }
1186
- let detectedPorts = [];
1187
- if (isRunning) {
1188
- detectedPorts = await getProcessPorts(existingProcess.pid);
1589
+ } else if (restartCount > 0) {
1590
+ const now = Date.now();
1591
+ if (!lastSeenAliveAt) {
1592
+ lastSeenAliveAt = now;
1593
+ } else if (now - lastSeenAliveAt >= STABILITY_WINDOW_MS) {
1594
+ restartCount = 0;
1595
+ nextRestartAt = 0;
1596
+ lastSeenAliveAt = 0;
1597
+ }
1598
+ }
1599
+ await Bun.sleep(intervalMs);
1189
1600
  }
1190
- if (isRunning) {
1191
- await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
1192
- await terminateProcess(existingProcess.pid);
1193
- announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1194
- });
1601
+ } finally {
1602
+ releaseWatcherLock();
1603
+ await cleanupWatcher(targetName);
1604
+ }
1605
+ }
1606
+ function getGuardRestartCounts() {
1607
+ const counts = new Map;
1608
+ const entries = db.history.select().where({ event: "guard_restart" }).all();
1609
+ for (const entry of entries) {
1610
+ counts.set(entry.process_name, (counts.get(entry.process_name) || 0) + 1);
1611
+ }
1612
+ return counts;
1613
+ }
1614
+ function getRecentGuardEvents(limit = 100) {
1615
+ const rows = db.history.select().orderBy("timestamp", "desc").limit(limit * 4).all().filter((row) => row.event === "guard_restart" || row.event === "guard_restart_failed").slice(0, limit);
1616
+ return rows.map((row) => ({
1617
+ time: new Date(row.timestamp).getTime(),
1618
+ name: row.process_name,
1619
+ action: "restart",
1620
+ success: row.event === "guard_restart"
1621
+ }));
1622
+ }
1623
+
1624
+ // src/commands/run.ts
1625
+ var homePath2 = getHomeDir();
1626
+ var run = createMeasure2("run");
1627
+ var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1628
+ function resolveInternalBgrunCommand(command) {
1629
+ const trimmed = command.trim();
1630
+ if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
1631
+ return command;
1632
+ }
1633
+ if (trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
1634
+ return trimmed;
1635
+ }
1636
+ return `${INTERNAL_BUNX_PREFIX}${trimmed.slice("bgrun".length)}`;
1637
+ }
1638
+ async function handleRun(options) {
1639
+ const {
1640
+ command,
1641
+ directory,
1642
+ env,
1643
+ name,
1644
+ configPath,
1645
+ force,
1646
+ fetch,
1647
+ stdout,
1648
+ stderr
1649
+ } = options;
1650
+ const releaseOperationLock = name ? acquireProcessOperationLock(name) : () => {};
1651
+ try {
1652
+ const existingProcess = name ? getProcess(name) : null;
1653
+ if (name && existingProcess) {
1654
+ const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1655
+ const unmet = await getUnmetDeps2(name);
1656
+ if (unmet.length > 0) {
1657
+ await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1658
+ for (const depName of unmet) {
1659
+ const depProc = getProcess(depName);
1660
+ if (depProc) {
1661
+ announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1662
+ await handleRun({
1663
+ action: "run",
1664
+ name: depName,
1665
+ force: true,
1666
+ remoteName: ""
1667
+ });
1668
+ }
1669
+ }
1670
+ });
1671
+ }
1195
1672
  }
1196
- if (detectedPorts.length > 0) {
1197
- await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1198
- for (const port of detectedPorts) {
1199
- await killProcessOnPort(port);
1673
+ if (existingProcess) {
1674
+ const finalDirectory2 = directory || existingProcess.workdir;
1675
+ validateDirectory(finalDirectory2);
1676
+ $2.cwd(finalDirectory2);
1677
+ if (fetch) {
1678
+ if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1679
+ error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1200
1680
  }
1201
- for (const port of detectedPorts) {
1202
- const freed = await waitForPortFree(port, 5000);
1203
- if (!freed) {
1204
- await killProcessOnPort(port);
1205
- await waitForPortFree(port, 3000);
1681
+ await run.measure(`Git fetch "${name}"`, async () => {
1682
+ try {
1683
+ await $2`git fetch origin`;
1684
+ const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1685
+ const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1686
+ if (localHash !== remoteHash) {
1687
+ await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1688
+ announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1689
+ }
1690
+ } catch (err) {
1691
+ error(`Failed to pull latest changes: ${err}`);
1692
+ }
1693
+ });
1694
+ }
1695
+ const isRunning = await isProcessRunning(existingProcess.pid);
1696
+ if (isRunning && !force) {
1697
+ error(`Process '${name}' is currently running. Use --force to restart.`);
1698
+ }
1699
+ let actualPid2 = existingProcess.pid;
1700
+ if (!isRunning && !force) {
1701
+ const reconciled = await reconcileProcessPids([
1702
+ {
1703
+ name,
1704
+ pid: existingProcess.pid,
1705
+ command: existingProcess.command,
1706
+ workdir: existingProcess.workdir
1206
1707
  }
1708
+ ], new Set([existingProcess.pid]));
1709
+ const newPid = reconciled.get(name);
1710
+ if (newPid) {
1711
+ console.log(`[run] Reconciled dead PID ${existingProcess.pid} to live PID ${newPid}`);
1712
+ actualPid2 = newPid;
1713
+ updateProcessPid(name, newPid);
1207
1714
  }
1208
- });
1209
- }
1210
- const cmdToMatch = existingProcess.command;
1211
- if (cmdToMatch) {
1212
- await run.measure("Zombie sweep", async () => {
1213
- try {
1214
- const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1215
- const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1216
- if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1217
- return;
1715
+ }
1716
+ let detectedPorts = [];
1717
+ const actuallyRunning = await isProcessRunning(actualPid2);
1718
+ if (actuallyRunning) {
1719
+ detectedPorts = await getProcessPorts(actualPid2);
1720
+ }
1721
+ if (actuallyRunning) {
1722
+ await run.measure(`Terminate "${name}" (PID ${actualPid2})`, async () => {
1723
+ await terminateProcess(actualPid2);
1724
+ announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1725
+ });
1726
+ }
1727
+ if (detectedPorts.length > 0) {
1728
+ await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1729
+ for (const port of detectedPorts) {
1730
+ await killProcessOnPort(port);
1218
1731
  }
1219
- const currentPid = process.pid;
1220
- 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);
1221
- const zombiePids = result.split(`
1222
- `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1223
- for (const zPid of zombiePids) {
1224
- await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1732
+ for (const port of detectedPorts) {
1733
+ const freed = await waitForPortFree(port, 5000);
1734
+ if (!freed) {
1735
+ await killProcessOnPort(port);
1736
+ await waitForPortFree(port, 3000);
1737
+ }
1225
1738
  }
1226
- if (zombiePids.length > 0) {
1227
- announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1739
+ });
1740
+ }
1741
+ const existingEnv = existingProcess.env ? parseEnvString(existingProcess.env) : {};
1742
+ const declaredPort = getDeclaredPort(existingEnv, existingProcess.command);
1743
+ if (declaredPort && !detectedPorts.includes(declaredPort)) {
1744
+ await run.measure(`Declared port cleanup [${declaredPort}]`, async () => {
1745
+ const portFree = await isPortFree(declaredPort);
1746
+ if (!portFree) {
1747
+ console.log(`[run] Declared port ${declaredPort} is busy (orphaned process), cleaning up...`);
1748
+ await killProcessOnPort(declaredPort);
1749
+ const freed = await waitForPortFree(declaredPort, 5000);
1750
+ if (!freed) {
1751
+ console.warn(`[run] Port ${declaredPort} still busy after cleanup, retrying...`);
1752
+ await killProcessOnPort(declaredPort);
1753
+ await waitForPortFree(declaredPort, 3000);
1754
+ }
1228
1755
  }
1229
- } catch {}
1230
- });
1231
- }
1232
- await retryDatabaseOperation(() => removeProcessByName(name));
1233
- } else {
1234
- if (!directory || !name || !command) {
1235
- error("'directory', 'name', and 'command' parameters are required for new processes.");
1236
- }
1237
- validateDirectory(directory);
1238
- $2.cwd(directory);
1239
- }
1240
- const finalCommand = command || existingProcess.command;
1241
- const finalDirectory = directory || existingProcess?.workdir;
1242
- let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1243
- if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1244
- finalEnv.BGR_KEEP_ALIVE = "true";
1245
- }
1246
- let finalConfigPath;
1247
- if (configPath !== undefined) {
1248
- finalConfigPath = configPath;
1249
- } else if (existingProcess) {
1250
- finalConfigPath = existingProcess.configPath;
1251
- } else {
1252
- finalConfigPath = ".config.toml";
1253
- }
1254
- if (finalConfigPath) {
1255
- const fullConfigPath = join3(finalDirectory, finalConfigPath);
1256
- if (await Bun.file(fullConfigPath).exists()) {
1257
- const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1258
- try {
1259
- return await parseConfigFile(fullConfigPath);
1260
- } catch (err) {
1261
- console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1262
- return null;
1263
- }
1264
- });
1265
- if (configEnv) {
1266
- finalEnv = { ...finalEnv, ...configEnv };
1267
- console.log(`Loaded config from ${finalConfigPath}`);
1756
+ });
1268
1757
  }
1758
+ const cmdToMatch = existingProcess.command;
1759
+ if (cmdToMatch) {
1760
+ await run.measure("Zombie sweep", async () => {
1761
+ try {
1762
+ const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1763
+ const GENERIC_KEYWORDS = [
1764
+ "dev",
1765
+ "run",
1766
+ "start",
1767
+ "serve",
1768
+ "build",
1769
+ "test"
1770
+ ];
1771
+ if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1772
+ return;
1773
+ }
1774
+ const currentPid = process.pid;
1775
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -like '*${cmdKeyword.replace(/'/g, "''").replace(/([&[\](){}^$|\\*?+])/g, "$&")}*' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
1776
+ const zombiePids = result.split(`
1777
+ `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1778
+ for (const zPid of zombiePids) {
1779
+ await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1780
+ }
1781
+ if (zombiePids.length > 0) {
1782
+ announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1783
+ }
1784
+ } catch {}
1785
+ });
1786
+ }
1787
+ await retryDatabaseOperation(() => removeProcessByName(name));
1269
1788
  } else {
1270
- console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1271
- }
1272
- }
1273
- const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
1274
- Bun.write(stdoutPath, "");
1275
- const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
1276
- Bun.write(stderrPath, "");
1277
- const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1278
- const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1279
- env: { ...Bun.env, ...finalEnv },
1280
- cwd: finalDirectory,
1281
- stdout: Bun.file(stdoutPath),
1282
- stderr: Bun.file(stderrPath)
1283
- });
1284
- newProcess.unref();
1285
- await sleep2(100);
1286
- const pid = await findChildPid(newProcess.pid);
1287
- await sleep2(400);
1288
- return pid;
1289
- }) ?? 0;
1290
- await retryDatabaseOperation(() => insertProcess({
1291
- pid: actualPid,
1292
- workdir: finalDirectory,
1293
- command: finalCommand,
1294
- name,
1295
- env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
1296
- configPath: finalConfigPath || "",
1297
- stdout_path: stdoutPath,
1298
- stderr_path: stderrPath
1299
- }));
1300
- announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1301
- }
1302
- var homePath2, run;
1303
- var init_run = __esm(() => {
1304
- init_db();
1305
- init_platform();
1306
- init_logger();
1307
- init_utils();
1308
- homePath2 = getHomeDir();
1309
- run = createMeasure2("run");
1310
- });
1311
-
1312
- // src/server.ts
1313
- var exports_server = {};
1314
- __export(exports_server, {
1315
- startServer: () => startServer,
1316
- guardRestartCounts: () => guardRestartCounts,
1317
- guardEvents: () => guardEvents
1318
- });
1319
- import path from "path";
1320
- async function cleanupPort(port) {
1321
- if (process.platform !== "win32")
1322
- return port;
1323
- try {
1324
- const proc = Bun.spawn([
1325
- "powershell",
1326
- "-NoProfile",
1327
- "-Command",
1328
- `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1329
- ], { stdout: "pipe", stderr: "pipe" });
1330
- const text = await new Response(proc.stdout).text();
1331
- const pid = parseInt(text.trim(), 10);
1332
- if (!pid || pid === process.pid)
1333
- return port;
1334
- const checkProc = Bun.spawn([
1335
- "powershell",
1336
- "-NoProfile",
1337
- "-Command",
1338
- `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`
1339
- ], { stdout: "pipe", stderr: "pipe" });
1340
- const checkText = await new Response(checkProc.stdout).text();
1341
- if (checkText.trim()) {
1342
- console.log(`[server] Killing PID ${pid} holding port ${port}`);
1343
- Bun.spawn(["taskkill", "/F", "/PID", String(pid)], { stdout: "pipe", stderr: "pipe" });
1344
- await Bun.sleep(1000);
1345
- return port;
1789
+ if (!directory || !name || !command) {
1790
+ error("'directory', 'name', and 'command' parameters are required for new processes.");
1791
+ }
1792
+ validateDirectory(directory);
1793
+ $2.cwd(directory);
1794
+ }
1795
+ const storedCommand = command || existingProcess.command;
1796
+ const finalCommand = resolveInternalBgrunCommand(storedCommand);
1797
+ const finalDirectory = directory || existingProcess?.workdir;
1798
+ let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1799
+ let finalConfigPath;
1800
+ if (configPath !== undefined) {
1801
+ finalConfigPath = configPath;
1802
+ } else if (existingProcess) {
1803
+ finalConfigPath = existingProcess.configPath;
1346
1804
  } else {
1347
- const fallback = port + 1;
1348
- console.log(`[server] \u26A0 Port ${port} held by zombie PID ${pid} \u2014 falling back to port ${fallback}`);
1349
- return fallback;
1805
+ finalConfigPath = ".config.toml";
1350
1806
  }
1351
- } catch {
1352
- return port;
1353
- }
1354
- }
1355
- async function startServer() {
1356
- const { start } = await import("melina");
1357
- const appDir = path.join(import.meta.dir, "../dashboard/app");
1358
- const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
1359
- _originalPort = requestedPort;
1360
- const resolvedPort = await cleanupPort(requestedPort);
1361
- _currentPort = resolvedPort;
1362
- const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
1363
- await start({
1364
- appDir,
1365
- defaultTitle: "bgrun Dashboard - Process Manager",
1366
- globalCss: path.join(appDir, "globals.css"),
1367
- ...needsExplicitPort && { port: resolvedPort }
1368
- });
1369
- startGuard();
1370
- const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1371
- startLogRotation2(() => getAllProcesses());
1372
- if (resolvedPort !== requestedPort) {
1373
- startStickyPortChecker();
1374
- }
1375
- }
1376
- function startStickyPortChecker() {
1377
- const CHECK_INTERVAL_MS = 60000;
1378
- console.log(`[server] Starting sticky port checker (original: ${_originalPort}, current: ${_currentPort})`);
1379
- setInterval(async () => {
1380
- if (_currentPort === _originalPort)
1381
- return;
1382
- try {
1383
- const proc = Bun.spawn([
1384
- "powershell",
1385
- "-NoProfile",
1386
- "-Command",
1387
- `Get-NetTCPConnection -LocalPort ${_originalPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`
1388
- ], { stdout: "pipe", stderr: "pipe" });
1389
- const text = await new Response(proc.stdout).text();
1390
- const pid = parseInt(text.trim(), 10);
1391
- if (!pid) {
1392
- console.log(`[server] \u2713 Original port ${_originalPort} is now available! Consider restarting to reclaim it.`);
1393
- _currentPort = _originalPort;
1394
- }
1395
- } catch {}
1396
- }, CHECK_INTERVAL_MS);
1397
- }
1398
- function startGuard() {
1399
- console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
1400
- setInterval(async () => {
1401
- try {
1402
- const processes = getAllProcesses();
1403
- if (processes.length === 0)
1404
- return;
1405
- for (const proc of processes) {
1406
- if (GUARD_SKIP_NAMES.has(proc.name))
1407
- continue;
1408
- const env = proc.env ? parseEnvString(proc.env) : {};
1409
- if (env.BGR_KEEP_ALIVE !== "true")
1410
- continue;
1411
- const alive = await isProcessRunning(proc.pid, proc.command);
1412
- if (!alive) {
1413
- const now = Date.now();
1414
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1415
- if (now < nextRestart)
1416
- continue;
1417
- console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1418
- let success = false;
1807
+ if (finalConfigPath) {
1808
+ const fullConfigPath = join5(finalDirectory, finalConfigPath);
1809
+ if (await Bun.file(fullConfigPath).exists()) {
1810
+ const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1419
1811
  try {
1420
- await handleRun({
1421
- action: "run",
1422
- name: proc.name,
1423
- force: true,
1424
- remoteName: ""
1425
- });
1426
- success = true;
1427
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1428
- const newCount = prevCount + 1;
1429
- guardRestartCounts.set(proc.name, newCount);
1430
- try {
1431
- addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1432
- } catch {}
1433
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1434
- if (guardEvents.length > 100)
1435
- guardEvents.pop();
1436
- if (newCount > 5) {
1437
- const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
1438
- guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
1439
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
1440
- } else {
1441
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
1442
- }
1812
+ return await parseConfigFile(fullConfigPath);
1443
1813
  } catch (err) {
1444
- console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1445
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1446
- if (guardEvents.length > 100)
1447
- guardEvents.pop();
1448
- }
1449
- } else {
1450
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1451
- if (prevCount > 0) {
1452
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1453
- if (Date.now() > nextRestart + 60000) {
1454
- guardRestartCounts.delete(proc.name);
1455
- guardNextRestartTime.delete(proc.name);
1456
- }
1814
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1815
+ return null;
1457
1816
  }
1817
+ });
1818
+ if (configEnv) {
1819
+ finalEnv = { ...finalEnv, ...configEnv };
1820
+ console.log(`Loaded config from ${finalConfigPath}`);
1458
1821
  }
1822
+ } else {
1823
+ console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1459
1824
  }
1460
- } catch (err) {
1461
- console.error(`[guard] Error in guard loop: ${err.message}`);
1462
1825
  }
1463
- }, GUARD_INTERVAL_MS);
1464
- }
1465
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
1466
- var init_server = __esm(() => {
1467
- init_db();
1468
- init_platform();
1469
- init_run();
1470
- init_utils();
1471
- GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1472
- _g = globalThis;
1473
- if (!_g.__bgrGuardRestartCounts)
1474
- _g.__bgrGuardRestartCounts = new Map;
1475
- if (!_g.__bgrGuardNextRestartTime)
1476
- _g.__bgrGuardNextRestartTime = new Map;
1477
- if (!_g.__bgrGuardEvents)
1478
- _g.__bgrGuardEvents = [];
1479
- guardRestartCounts = _g.__bgrGuardRestartCounts;
1480
- guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1481
- guardEvents = _g.__bgrGuardEvents;
1482
- });
1483
-
1484
- // src/guard.ts
1485
- var exports_guard = {};
1486
- __export(exports_guard, {
1487
- startGuardLoop: () => startGuardLoop
1488
- });
1489
- import { createHmac } from "crypto";
1490
- async function notifyWebhook(event, name, details) {
1491
- if (!WEBHOOK_URL)
1492
- return;
1493
- try {
1494
- const payload = JSON.stringify({
1495
- event,
1496
- process: name,
1497
- timestamp: new Date().toISOString(),
1498
- ...details
1499
- });
1500
- const headers = {
1501
- "Content-Type": "application/json",
1502
- "User-Agent": "bgrun-guard/1.0"
1503
- };
1504
- if (WEBHOOK_SECRET) {
1505
- const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
1506
- headers["X-BGR-Signature"] = `sha256=${sig}`;
1507
- }
1508
- const controller = new AbortController;
1509
- const timeout = setTimeout(() => controller.abort(), 5000);
1510
- await fetch(WEBHOOK_URL, {
1511
- method: "POST",
1512
- headers,
1513
- body: payload,
1514
- signal: controller.signal
1515
- });
1516
- clearTimeout(timeout);
1517
- } catch (err) {
1518
- console.error(`[guard] Webhook failed: ${err.message}`);
1519
- }
1520
- }
1521
- async function restartProcess(name) {
1522
- try {
1523
- await handleRun({
1524
- action: "run",
1826
+ const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1827
+ Bun.write(stdoutPath, "");
1828
+ const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1829
+ Bun.write(stderrPath, "");
1830
+ const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1831
+ const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1832
+ env: buildManagedProcessEnv(Bun.env, finalEnv),
1833
+ cwd: finalDirectory,
1834
+ stdout: Bun.file(stdoutPath),
1835
+ stderr: Bun.file(stderrPath)
1836
+ });
1837
+ newProcess.unref();
1838
+ await sleep2(100);
1839
+ const pid = await findChildPid(newProcess.pid);
1840
+ await sleep2(400);
1841
+ return pid;
1842
+ }) ?? 0;
1843
+ await retryDatabaseOperation(() => insertProcess({
1844
+ pid: actualPid,
1845
+ workdir: finalDirectory,
1846
+ command: finalCommand,
1525
1847
  name,
1526
- force: true,
1527
- remoteName: ""
1528
- });
1529
- return true;
1530
- } catch (err) {
1531
- console.error(`[guard] \u2717 Failed to restart "${name}": ${err.message}`);
1532
- return false;
1533
- }
1534
- }
1535
- function getBackoffMs(restartCount) {
1536
- if (restartCount <= CRASH_THRESHOLD)
1537
- return 0;
1538
- const exponent = restartCount - CRASH_THRESHOLD;
1539
- return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
1540
- }
1541
- async function guardCycle() {
1542
- try {
1543
- const processes = getAllProcesses();
1544
- if (processes.length === 0)
1545
- return;
1546
- const now = Date.now();
1547
- let checked = 0;
1548
- let restarted = 0;
1549
- let skipped = 0;
1550
- for (const proc of processes) {
1551
- if (proc.name === "bgr-guard")
1552
- continue;
1553
- const env = proc.env ? parseEnvString(proc.env) : {};
1554
- const isGuarded = env.BGR_KEEP_ALIVE === "true";
1555
- const isDashboard = proc.name === "bgr-dashboard";
1556
- if (!isGuarded && !isDashboard)
1557
- continue;
1558
- checked++;
1559
- try {
1560
- const alive = await isProcessRunning(proc.pid, proc.command);
1561
- if (!alive && proc.pid > 0) {
1562
- const nextRestart = state.nextRestartTime.get(proc.name) || 0;
1563
- if (now < nextRestart) {
1564
- const waitSecs = Math.round((nextRestart - now) / 1000);
1565
- skipped++;
1566
- continue;
1567
- }
1568
- console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
1569
- notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
1570
- const success = await restartProcess(proc.name);
1571
- if (success) {
1572
- const count = (state.restartCounts.get(proc.name) || 0) + 1;
1573
- state.restartCounts.set(proc.name, count);
1574
- state.lastSeenAlive.delete(proc.name);
1575
- const backoff = getBackoffMs(count);
1576
- if (backoff > 0) {
1577
- state.nextRestartTime.set(proc.name, now + backoff);
1578
- console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
1579
- } else {
1580
- console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
1581
- }
1582
- restarted++;
1583
- notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
1584
- } else {
1585
- notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
1586
- }
1587
- } else if (alive) {
1588
- const count = state.restartCounts.get(proc.name) || 0;
1589
- if (count > 0) {
1590
- const lastSeen = state.lastSeenAlive.get(proc.name);
1591
- if (!lastSeen) {
1592
- state.lastSeenAlive.set(proc.name, now);
1593
- } else if (now - lastSeen > STABILITY_WINDOW_MS) {
1594
- state.restartCounts.delete(proc.name);
1595
- state.nextRestartTime.delete(proc.name);
1596
- state.lastSeenAlive.delete(proc.name);
1597
- console.log(`[guard] \u2713 "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s \u2014 reset counters`);
1598
- }
1599
- }
1600
- }
1601
- } catch (err) {
1602
- console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
1603
- }
1604
- }
1605
- if (restarted > 0) {
1606
- console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
1848
+ env: stringifyEnvString(finalEnv),
1849
+ configPath: finalConfigPath || "",
1850
+ stdout_path: stdoutPath,
1851
+ stderr_path: stderrPath
1852
+ }));
1853
+ if (!isInternalProcessName(name)) {
1854
+ await syncProcessWatcher(name, finalEnv);
1607
1855
  }
1608
- } catch (err) {
1609
- console.error(`[guard] Error in guard cycle: ${err.message}`);
1856
+ announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1857
+ } finally {
1858
+ releaseOperationLock();
1610
1859
  }
1611
1860
  }
1612
- async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
1613
- const interval = intervalMs || DEFAULT_INTERVAL_MS;
1614
- console.log(`[guard] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
1615
- console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
1616
- console.log(`[guard] Check interval: ${interval / 1000}s`);
1617
- console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
1618
- console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
1619
- console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
1620
- console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
1621
- console.log(`[guard] Started: ${new Date().toLocaleString()}`);
1622
- 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`);
1623
- await guardCycle();
1624
- setInterval(guardCycle, interval);
1625
- }
1626
- var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1627
- var init_guard = __esm(() => {
1628
- init_db();
1629
- init_platform();
1630
- init_run();
1631
- init_utils();
1632
- WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
1633
- WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
1634
- MAX_BACKOFF_MS = 5 * 60000;
1635
- state = {
1636
- restartCounts: new Map,
1637
- nextRestartTime: new Map,
1638
- lastSeenAlive: new Map
1639
- };
1640
- });
1641
-
1642
- // src/index.ts
1643
- init_utils();
1644
- init_run();
1645
- import { parseArgs } from "util";
1646
1861
 
1647
1862
  // src/commands/list.ts
1648
1863
  import chalk3 from "chalk";
@@ -1812,7 +2027,6 @@ function renderProcessTable(processes, options) {
1812
2027
 
1813
2028
  // src/commands/list.ts
1814
2029
  init_db();
1815
- init_logger();
1816
2030
  init_utils();
1817
2031
  init_platform();
1818
2032
  function formatMemory(bytes) {
@@ -1826,6 +2040,8 @@ function formatMemory(bytes) {
1826
2040
  async function showAll(opts) {
1827
2041
  const processes = getAllProcesses();
1828
2042
  const filtered = processes.filter((proc) => {
2043
+ if (isInternalProcessName(proc.name))
2044
+ return false;
1829
2045
  if (!opts?.filter)
1830
2046
  return true;
1831
2047
  const envVars = parseEnvString(proc.env);
@@ -1873,11 +2089,21 @@ async function showAll(opts) {
1873
2089
  for (const proc of filtered) {
1874
2090
  const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1875
2091
  const runtime = calculateRuntime(proc.timestamp);
1876
- const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1877
- const ports = isRunning ? await getProcessPorts(proc.pid) : [];
2092
+ let displayPid = proc.pid;
2093
+ let ports = [];
2094
+ if (isRunning) {
2095
+ const resolved = await resolvePidWithPorts(proc.pid);
2096
+ displayPid = resolved.pid;
2097
+ ports = resolved.ports;
2098
+ if (displayPid !== proc.pid) {
2099
+ updateProcessPid(proc.name, displayPid);
2100
+ proc.pid = displayPid;
2101
+ }
2102
+ }
2103
+ const mem = isRunning ? resourceMap.get(displayPid)?.memory || resourceMap.get(proc.pid)?.memory || 0 : 0;
1878
2104
  tableData.push({
1879
2105
  id: proc.id,
1880
- pid: proc.pid,
2106
+ pid: displayPid,
1881
2107
  name: proc.name,
1882
2108
  port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
1883
2109
  memory: formatMemory(mem),
@@ -1909,7 +2135,7 @@ async function showAll(opts) {
1909
2135
  // src/commands/cleanup.ts
1910
2136
  init_db();
1911
2137
  init_platform();
1912
- init_logger();
2138
+ init_utils();
1913
2139
  import * as fs3 from "fs";
1914
2140
  async function handleDelete(name) {
1915
2141
  const process2 = getProcess(name);
@@ -1921,6 +2147,9 @@ async function handleDelete(name) {
1921
2147
  if (isRunning) {
1922
2148
  await terminateProcess(process2.pid);
1923
2149
  }
2150
+ if (!isInternalProcessName(name)) {
2151
+ await stopProcessWatcher(name);
2152
+ }
1924
2153
  if (fs3.existsSync(process2.stdout_path)) {
1925
2154
  try {
1926
2155
  fs3.unlinkSync(process2.stdout_path);
@@ -1941,6 +2170,12 @@ async function handleClean() {
1941
2170
  for (const proc of processes) {
1942
2171
  const running = await isProcessRunning(proc.pid);
1943
2172
  if (!running) {
2173
+ const watched = getWatchedProcessName(proc.name);
2174
+ if (watched) {
2175
+ removeProcess(proc.pid);
2176
+ cleanedCount++;
2177
+ continue;
2178
+ }
1944
2179
  removeProcess(proc.pid);
1945
2180
  cleanedCount++;
1946
2181
  if (fs3.existsSync(proc.stdout_path)) {
@@ -1969,18 +2204,33 @@ async function handleStop(name) {
1969
2204
  error(`No process found named '${name}'`);
1970
2205
  return;
1971
2206
  }
1972
- const isRunning = await isProcessRunning(proc.pid);
1973
- if (!isRunning) {
1974
- announce(`Process '${name}' is already stopped.`, "Process Stop");
1975
- return;
1976
- }
1977
- const ports = await getProcessPorts(proc.pid);
1978
- await terminateProcess(proc.pid);
1979
- for (const port of ports) {
1980
- await killProcessOnPort(port);
2207
+ const releaseOperationLock = acquireProcessOperationLock(name);
2208
+ try {
2209
+ const isRunning = await isProcessRunning(proc.pid);
2210
+ if (!isRunning) {
2211
+ announce(`Process '${name}' is already stopped.`, "Process Stop");
2212
+ return;
2213
+ }
2214
+ const ports = await getProcessPorts(proc.pid);
2215
+ await terminateProcess(proc.pid);
2216
+ for (const port of ports) {
2217
+ await killProcessOnPort(port);
2218
+ }
2219
+ const procEnv = proc.env ? parseEnvString(proc.env) : {};
2220
+ const declaredPort = getDeclaredPort(procEnv, proc.command);
2221
+ if (declaredPort && !ports.includes(declaredPort)) {
2222
+ const portFree = await isPortFree(declaredPort);
2223
+ if (!portFree) {
2224
+ console.log(`[stop] Declared port ${declaredPort} is busy (orphaned process), cleaning up...`);
2225
+ await killProcessOnPort(declaredPort);
2226
+ await waitForPortFree(declaredPort, 3000);
2227
+ }
2228
+ }
2229
+ updateProcessPid(name, 0);
2230
+ announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
2231
+ } finally {
2232
+ releaseOperationLock();
1981
2233
  }
1982
- updateProcessPid(name, 0);
1983
- announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
1984
2234
  }
1985
2235
  async function handleDeleteAll() {
1986
2236
  const processes = getAllProcesses();
@@ -1991,6 +2241,9 @@ async function handleDeleteAll() {
1991
2241
  let killedCount = 0;
1992
2242
  let portsFreed = 0;
1993
2243
  for (const proc of processes) {
2244
+ if (!isInternalProcessName(proc.name)) {
2245
+ await stopProcessWatcher(proc.name);
2246
+ }
1994
2247
  const running = await isProcessRunning(proc.pid);
1995
2248
  if (running) {
1996
2249
  const ports = await getProcessPorts(proc.pid);
@@ -2006,6 +2259,18 @@ async function handleDeleteAll() {
2006
2259
  portsFreed++;
2007
2260
  }
2008
2261
  }
2262
+ const procEnv = proc.env ? parseEnvString(proc.env) : {};
2263
+ const declaredPort = getDeclaredPort(procEnv, proc.command);
2264
+ if (declaredPort) {
2265
+ const portFree = await isPortFree(declaredPort);
2266
+ if (!portFree) {
2267
+ console.log(`[nuke] Declared port ${declaredPort} is busy, cleaning up...`);
2268
+ await killProcessOnPort(declaredPort);
2269
+ const freed = await waitForPortFree(declaredPort, 3000);
2270
+ if (freed)
2271
+ portsFreed++;
2272
+ }
2273
+ }
2009
2274
  if (fs3.existsSync(proc.stdout_path)) {
2010
2275
  try {
2011
2276
  fs3.unlinkSync(proc.stdout_path);
@@ -2029,9 +2294,7 @@ async function handleDeleteAll() {
2029
2294
  // src/commands/watch.ts
2030
2295
  init_db();
2031
2296
  init_platform();
2032
- init_logger();
2033
2297
  init_utils();
2034
- init_run();
2035
2298
  import * as fs4 from "fs";
2036
2299
  import path2 from "path";
2037
2300
  import chalk4 from "chalk";
@@ -2221,7 +2484,6 @@ SIGINT received...`));
2221
2484
 
2222
2485
  // src/commands/logs.ts
2223
2486
  init_db();
2224
- init_logger();
2225
2487
  init_platform();
2226
2488
  import chalk5 from "chalk";
2227
2489
  import * as fs5 from "fs";
@@ -2266,7 +2528,6 @@ async function showLogs(name, logType = "both", lines) {
2266
2528
  }
2267
2529
 
2268
2530
  // src/commands/details.ts
2269
- init_logger();
2270
2531
  init_db();
2271
2532
  init_utils();
2272
2533
  init_platform();
@@ -2277,6 +2538,10 @@ async function showDetails(name) {
2277
2538
  error(`No process found named '${name}'`);
2278
2539
  return;
2279
2540
  }
2541
+ if (isInternalProcessName(proc.name)) {
2542
+ error(`'${name}' is an internal bgrun process.`);
2543
+ return;
2544
+ }
2280
2545
  let isRunning = await isProcessRunning(proc.pid, proc.command);
2281
2546
  if (!isRunning && proc.pid > 0) {
2282
2547
  const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
@@ -2289,7 +2554,15 @@ async function showDetails(name) {
2289
2554
  }
2290
2555
  const runtime = calculateRuntime(proc.timestamp);
2291
2556
  const envVars = parseEnvString(proc.env);
2292
- const ports = isRunning ? await getProcessPorts(proc.pid) : [];
2557
+ let ports = [];
2558
+ if (isRunning) {
2559
+ const resolved = await resolvePidWithPorts(proc.pid);
2560
+ ports = resolved.ports;
2561
+ if (resolved.pid !== proc.pid) {
2562
+ updateProcessPid(proc.name, resolved.pid);
2563
+ proc.pid = resolved.pid;
2564
+ }
2565
+ }
2293
2566
  const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
2294
2567
  const details = `
2295
2568
  ${chalk6.bold("Process Details:")}
@@ -2313,13 +2586,225 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${ch
2313
2586
  announce(details, `Process Details: ${name}`);
2314
2587
  }
2315
2588
 
2589
+ // src/commands/envit.ts
2590
+ function parseEnvitArgs(args) {
2591
+ let directory;
2592
+ let configPath;
2593
+ let shell;
2594
+ let help = false;
2595
+ for (let i = 0;i < args.length; i++) {
2596
+ const arg = args[i];
2597
+ if (arg === "--help" || arg === "-h") {
2598
+ help = true;
2599
+ continue;
2600
+ }
2601
+ if (arg === "--directory") {
2602
+ directory = args[++i];
2603
+ if (!directory)
2604
+ error("Missing value for --directory.");
2605
+ continue;
2606
+ }
2607
+ if (arg.startsWith("--directory=")) {
2608
+ directory = arg.slice("--directory=".length);
2609
+ if (!directory)
2610
+ error("Missing value for --directory.");
2611
+ continue;
2612
+ }
2613
+ if (arg === "--config") {
2614
+ configPath = args[++i];
2615
+ if (!configPath)
2616
+ error("Missing value for --config.");
2617
+ continue;
2618
+ }
2619
+ if (arg.startsWith("--config=")) {
2620
+ configPath = arg.slice("--config=".length);
2621
+ if (!configPath)
2622
+ error("Missing value for --config.");
2623
+ continue;
2624
+ }
2625
+ if (arg === "--shell") {
2626
+ const value = args[++i];
2627
+ if (!value)
2628
+ error("Missing value for --shell.");
2629
+ shell = normalizeEnvitShell(value);
2630
+ continue;
2631
+ }
2632
+ if (arg.startsWith("--shell=")) {
2633
+ shell = normalizeEnvitShell(arg.slice("--shell=".length));
2634
+ continue;
2635
+ }
2636
+ if (!configPath) {
2637
+ configPath = arg;
2638
+ continue;
2639
+ }
2640
+ error(`Unexpected argument '${arg}'. envit prints shell export commands and does not run a child process.`);
2641
+ }
2642
+ return { directory, configPath, shell, help };
2643
+ }
2644
+ function normalizeEnvitShell(value) {
2645
+ const normalized = value.trim().toLowerCase();
2646
+ if (normalized === "powershell" || normalized === "pwsh" || normalized === "ps1")
2647
+ return "powershell";
2648
+ if (normalized === "cmd" || normalized === "bat")
2649
+ return "cmd";
2650
+ if (normalized === "sh" || normalized === "bash" || normalized === "zsh")
2651
+ return "sh";
2652
+ if (normalized === "json")
2653
+ return "json";
2654
+ error(`Unsupported shell '${value}'. Use powershell, cmd, sh, or json.`);
2655
+ }
2656
+ function detectEnvitShell() {
2657
+ if (process.platform === "win32") {
2658
+ return "powershell";
2659
+ }
2660
+ return "sh";
2661
+ }
2662
+ function escapePowerShell(value) {
2663
+ return `'${value.replace(/'/g, "''")}'`;
2664
+ }
2665
+ function escapeCmd(value) {
2666
+ return value.replace(/"/g, '""');
2667
+ }
2668
+ function escapeSh(value) {
2669
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2670
+ }
2671
+ function renderEnvitOutput(env, shell) {
2672
+ if (shell === "json") {
2673
+ return JSON.stringify(env, null, 2);
2674
+ }
2675
+ const lines = Object.entries(env).map(([key, value]) => {
2676
+ if (shell === "powershell") {
2677
+ return `$env:${key}=${escapePowerShell(value)}`;
2678
+ }
2679
+ if (shell === "cmd") {
2680
+ return `set "${key}=${escapeCmd(value)}"`;
2681
+ }
2682
+ return `export ${key}=${escapeSh(value)}`;
2683
+ });
2684
+ return lines.join(`
2685
+ `);
2686
+ }
2687
+ async function handleEnvit(options) {
2688
+ const cwd = options.directory || process.cwd();
2689
+ const configPath = options.configPath || ".config.toml";
2690
+ const shell = options.shell || detectEnvitShell();
2691
+ const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
2692
+ if (!exists) {
2693
+ error(`Config file '${configPath}' not found.`);
2694
+ }
2695
+ console.log(renderEnvitOutput(configEnv, shell));
2696
+ }
2697
+
2698
+ // src/commands/inline.ts
2699
+ init_utils();
2700
+ function parseInlineArgs(args) {
2701
+ let directory;
2702
+ let configPath;
2703
+ let help = false;
2704
+ const commandArgs = [];
2705
+ for (let i = 0;i < args.length; i++) {
2706
+ const arg = args[i];
2707
+ if (commandArgs.length > 0) {
2708
+ commandArgs.push(arg);
2709
+ continue;
2710
+ }
2711
+ if (arg === "--") {
2712
+ commandArgs.push(...args.slice(i + 1));
2713
+ break;
2714
+ }
2715
+ if (arg === "--help" || arg === "-h") {
2716
+ help = true;
2717
+ continue;
2718
+ }
2719
+ if (arg === "--directory") {
2720
+ directory = args[++i];
2721
+ if (!directory)
2722
+ error("Missing value for --directory.");
2723
+ continue;
2724
+ }
2725
+ if (arg.startsWith("--directory=")) {
2726
+ directory = arg.slice("--directory=".length);
2727
+ if (!directory)
2728
+ error("Missing value for --directory.");
2729
+ continue;
2730
+ }
2731
+ if (arg === "--config") {
2732
+ configPath = args[++i];
2733
+ if (!configPath)
2734
+ error("Missing value for --config.");
2735
+ continue;
2736
+ }
2737
+ if (arg.startsWith("--config=")) {
2738
+ configPath = arg.slice("--config=".length);
2739
+ if (!configPath)
2740
+ error("Missing value for --config.");
2741
+ continue;
2742
+ }
2743
+ commandArgs.push(...args.slice(i));
2744
+ break;
2745
+ }
2746
+ return { directory, configPath, commandArgs, help };
2747
+ }
2748
+ async function handleInline(options) {
2749
+ const cwd = options.directory || process.cwd();
2750
+ const configPath = options.configPath || ".config.toml";
2751
+ const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
2752
+ if (exists) {
2753
+ console.log(`Loaded config from ${configPath}`);
2754
+ } else {
2755
+ console.log(`Config file '${configPath}' not found, continuing without it.`);
2756
+ }
2757
+ if (options.commandArgs.length === 0) {
2758
+ error("Please provide a command to run. Example: bgrun inline -- bun run dev");
2759
+ }
2760
+ const proc = Bun.spawn(options.commandArgs, {
2761
+ cwd,
2762
+ env: buildManagedProcessEnv(Bun.env, configEnv),
2763
+ stdin: "inherit",
2764
+ stdout: "inherit",
2765
+ stderr: "inherit"
2766
+ });
2767
+ const exitCode = await proc.exited;
2768
+ process.exit(exitCode);
2769
+ }
2770
+
2771
+ // src/commands/guard.ts
2772
+ init_db();
2773
+ init_utils();
2774
+ async function handleGuardToggle(targetName, enabled) {
2775
+ if (!targetName) {
2776
+ error(`Please provide a process name. Example: bunx bgrun myapp ${enabled ? "--guard" : "--guard-off"}`);
2777
+ }
2778
+ if (isInternalProcessName(targetName)) {
2779
+ error(`'${targetName}' is an internal bgrun process.`);
2780
+ }
2781
+ const proc = getProcess(targetName);
2782
+ if (!proc) {
2783
+ error(`Process '${targetName}' not found.`);
2784
+ }
2785
+ const env = parseEnvString(proc.env || "");
2786
+ const alreadyGuarded = env.BGR_KEEP_ALIVE === "true";
2787
+ if (enabled) {
2788
+ env.BGR_KEEP_ALIVE = "true";
2789
+ await retryDatabaseOperation(() => updateProcessEnv(targetName, stringifyEnvString(env)));
2790
+ await ensureProcessWatcher(targetName);
2791
+ addHistoryEntry(targetName, "guard_on", proc.pid);
2792
+ announce(alreadyGuarded ? `Guard is already active for '${targetName}'` : `\uD83D\uDEE1\uFE0F Guard enabled for '${targetName}'`, "Process Guard");
2793
+ return;
2794
+ }
2795
+ delete env.BGR_KEEP_ALIVE;
2796
+ await retryDatabaseOperation(() => updateProcessEnv(targetName, stringifyEnvString(env)));
2797
+ await stopProcessWatcher(targetName);
2798
+ addHistoryEntry(targetName, "guard_off", proc.pid);
2799
+ announce(alreadyGuarded ? `\uD83D\uDED1 Guard disabled for '${targetName}'` : `Guard is already off for '${targetName}'`, "Process Guard");
2800
+ }
2801
+
2316
2802
  // src/index.ts
2317
- init_logger();
2318
2803
  init_platform();
2319
2804
  init_db();
2320
2805
  import dedent from "dedent";
2321
2806
  import chalk7 from "chalk";
2322
- import { join as join4 } from "path";
2807
+ import { join as join6 } from "path";
2323
2808
  var {sleep: sleep3 } = globalThis.Bun;
2324
2809
  import { configure } from "measure-fn";
2325
2810
  if (!Bun.argv.includes("--_serve")) {
@@ -2367,32 +2852,49 @@ function redirectConsoleToFiles() {
2367
2852
  };
2368
2853
  }
2369
2854
  }
2855
+ async function findDetachedProcessByArg(snippet) {
2856
+ if (process.platform !== "win32")
2857
+ return null;
2858
+ try {
2859
+ const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${snippet.replace(/'/g, "''")}' } | Sort-Object -Property CreationDate -Descending | Select-Object -First 1 -ExpandProperty ProcessId`, 3000);
2860
+ const pid = parseInt(result.trim(), 10);
2861
+ return !isNaN(pid) && pid > 0 ? pid : null;
2862
+ } catch {
2863
+ return null;
2864
+ }
2865
+ }
2370
2866
  async function showHelp() {
2371
2867
  const usage = dedent`
2372
2868
  ${chalk7.bold("bgrun \u2014 Bun Background Runner")}
2373
2869
  ${chalk7.gray("\u2550".repeat(50))}
2374
2870
 
2375
2871
  ${chalk7.yellow("Usage:")}
2376
- bgrun [name] [options]
2872
+ bunx bgrun [name] [options]
2377
2873
 
2378
2874
  ${chalk7.yellow("Commands:")}
2379
- bgrun List all processes
2380
- bgrun [name] Show details for a process
2381
- bgrun --dashboard Launch web dashboard (managed by bgrun)
2382
- bgrun --guard Launch standalone process guard
2383
- bgrun --restart [name] Restart a process
2384
- bgrun --restart-all Restart ALL registered processes
2385
- bgrun --stop [name] Stop a process (keep in registry)
2386
- bgrun --stop-all Stop ALL running processes
2387
- bgrun --delete [name] Delete a process
2388
- bgrun --clean Remove all stopped processes
2389
- bgrun --nuke Delete ALL processes
2875
+ bunx bgrun List all processes
2876
+ bunx bgrun [name] Show details for a process
2877
+ bunx bgrun -- <cmd> Start a managed process with an auto-generated name
2878
+ bunx bgrun inline -- <cmd> Run a command in this terminal with config env loaded
2879
+ bunx bgrun --env Print shell commands to export config env vars
2880
+ bunx bgrun --dashboard Launch web dashboard (managed by bgrun)
2881
+ bunx bgrun [name] --guard Enable crash watcher for a process
2882
+ bunx bgrun [name] --guard-off Disable crash watcher for a process
2883
+ bunx bgrun --restart [name] Restart a process
2884
+ bunx bgrun --restart-all Restart ALL registered processes
2885
+ bunx bgrun --stop [name] Stop a process (keep in registry)
2886
+ bunx bgrun --stop-all Stop ALL running processes
2887
+ bunx bgrun --delete [name] Delete a process
2888
+ bunx bgrun --clean Remove all stopped processes
2889
+ bunx bgrun --nuke Delete ALL processes
2390
2890
 
2391
2891
  ${chalk7.yellow("Options:")}
2392
2892
  --name <string> Process name (required for new)
2393
2893
  --command <string> Process command (required for new)
2394
2894
  --directory <path> Working directory (required for new)
2395
2895
  --config <path> Config file (default: .config.toml)
2896
+ --env Print shell export commands from config and exit
2897
+ --shell <type> Shell for --env: powershell | cmd | sh | json
2396
2898
  --watch Watch for file changes and auto-restart
2397
2899
  --force Force restart existing process
2398
2900
  --fetch Fetch latest git changes before running
@@ -2405,85 +2907,187 @@ async function showHelp() {
2405
2907
  --version Show version
2406
2908
  --debug Show debug info (DB path, BGR home, etc.)
2407
2909
  --dashboard Launch web dashboard as bgrun-managed process
2910
+ --guard Enable per-process crash watcher
2911
+ --guard-off Disable per-process crash watcher
2408
2912
  --port <number> Port for dashboard (default: 3000)
2409
2913
  --help Show this help message
2410
2914
 
2411
2915
  ${chalk7.yellow("Examples:")}
2412
- bgrun --dashboard
2413
- bgrun --name myapp --command "bun run dev" --directory . --watch
2414
- bgrun myapp --logs --lines 50
2916
+ bunx bgrun -- bun run dev
2917
+ bunx bgrun --force -- bun run server.ts
2918
+ bunx bgrun inline -- bun run dev
2919
+ Invoke-Expression (bunx bgrun --env)
2920
+ eval "$(bunx bgrun --env --shell sh)"
2921
+ bunx bgrun --dashboard
2922
+ bunx bgrun myapp --guard
2923
+ bunx bgrun myapp --guard-off
2924
+ bunx bgrun --name myapp --command "bun run dev" --directory . --watch
2925
+ bunx bgrun myapp --logs --lines 50
2415
2926
  `;
2416
2927
  console.log(usage);
2417
2928
  }
2929
+ var cliArgOptions = {
2930
+ name: { type: "string" },
2931
+ command: { type: "string" },
2932
+ directory: { type: "string" },
2933
+ config: { type: "string" },
2934
+ env: { type: "boolean" },
2935
+ shell: { type: "string" },
2936
+ watch: { type: "boolean" },
2937
+ force: { type: "boolean" },
2938
+ fetch: { type: "boolean" },
2939
+ delete: { type: "boolean" },
2940
+ nuke: { type: "boolean" },
2941
+ restart: { type: "boolean" },
2942
+ "restart-all": { type: "boolean" },
2943
+ stop: { type: "boolean" },
2944
+ "stop-all": { type: "boolean" },
2945
+ clean: { type: "boolean" },
2946
+ json: { type: "boolean" },
2947
+ logs: { type: "boolean" },
2948
+ "log-stdout": { type: "boolean" },
2949
+ "log-stderr": { type: "boolean" },
2950
+ lines: { type: "string" },
2951
+ filter: { type: "string" },
2952
+ version: { type: "boolean" },
2953
+ help: { type: "boolean" },
2954
+ db: { type: "string" },
2955
+ stdout: { type: "string" },
2956
+ stderr: { type: "string" },
2957
+ dashboard: { type: "boolean" },
2958
+ guard: { type: "boolean" },
2959
+ "guard-off": { type: "boolean" },
2960
+ debug: { type: "boolean" },
2961
+ _serve: { type: "boolean" },
2962
+ "_watch-process": { type: "string" },
2963
+ port: { type: "string" }
2964
+ };
2418
2965
  async function run2() {
2966
+ const rawArgs = Bun.argv.slice(2);
2967
+ const isActionInvocation = (values2) => {
2968
+ 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);
2969
+ };
2970
+ if (rawArgs[0] === "inline") {
2971
+ const parsed = parseInlineArgs(rawArgs.slice(1));
2972
+ if (parsed.help) {
2973
+ console.log(dedent`
2974
+ ${chalk7.bold("bgrun inline")}
2975
+ ${chalk7.gray("\u2500".repeat(40))}
2976
+
2977
+ Run a command in the current terminal with env vars loaded from a bgrun config file.
2978
+
2979
+ Usage:
2980
+ bunx bgrun inline [--directory <path>] [--config <path>] -- <command> [args...]
2981
+
2982
+ Examples:
2983
+ bunx bgrun inline -- bun run dev
2984
+ bunx bgrun inline --directory apps/api -- node server.js
2985
+ `);
2986
+ return;
2987
+ }
2988
+ await handleInline(parsed);
2989
+ return;
2990
+ }
2991
+ const delimiterIndex = rawArgs.indexOf("--");
2992
+ if (delimiterIndex !== -1 && delimiterIndex < rawArgs.length - 1) {
2993
+ const preArgs = rawArgs.slice(0, delimiterIndex);
2994
+ const commandArgs = rawArgs.slice(delimiterIndex + 1);
2995
+ const { values: values2 } = parseArgs({
2996
+ args: preArgs,
2997
+ options: cliArgOptions,
2998
+ strict: false,
2999
+ allowPositionals: true
3000
+ });
3001
+ const autoName = values2.name || generateAutoProcessName();
3002
+ const inlineCommand = joinCommandArgs(commandArgs);
3003
+ const directory = values2.directory || process.cwd();
3004
+ await handleRun({
3005
+ action: "run",
3006
+ name: autoName,
3007
+ command: inlineCommand,
3008
+ directory,
3009
+ configPath: values2.config,
3010
+ force: values2.force,
3011
+ fetch: values2.fetch,
3012
+ remoteName: "",
3013
+ dbPath: values2.db,
3014
+ stdout: values2.stdout,
3015
+ stderr: values2.stderr
3016
+ });
3017
+ return;
3018
+ }
2419
3019
  const { values, positionals } = parseArgs({
2420
- args: Bun.argv.slice(2),
2421
- options: {
2422
- name: { type: "string" },
2423
- command: { type: "string" },
2424
- directory: { type: "string" },
2425
- config: { type: "string" },
2426
- watch: { type: "boolean" },
2427
- force: { type: "boolean" },
2428
- fetch: { type: "boolean" },
2429
- delete: { type: "boolean" },
2430
- nuke: { type: "boolean" },
2431
- restart: { type: "boolean" },
2432
- "restart-all": { type: "boolean" },
2433
- stop: { type: "boolean" },
2434
- "stop-all": { type: "boolean" },
2435
- clean: { type: "boolean" },
2436
- json: { type: "boolean" },
2437
- logs: { type: "boolean" },
2438
- "log-stdout": { type: "boolean" },
2439
- "log-stderr": { type: "boolean" },
2440
- lines: { type: "string" },
2441
- filter: { type: "string" },
2442
- version: { type: "boolean" },
2443
- help: { type: "boolean" },
2444
- db: { type: "string" },
2445
- stdout: { type: "string" },
2446
- stderr: { type: "string" },
2447
- dashboard: { type: "boolean" },
2448
- guard: { type: "boolean" },
2449
- debug: { type: "boolean" },
2450
- _serve: { type: "boolean" },
2451
- "_guard-loop": { type: "boolean" },
2452
- port: { type: "string" }
2453
- },
3020
+ args: rawArgs,
3021
+ options: cliArgOptions,
2454
3022
  strict: false,
2455
3023
  allowPositionals: true
2456
3024
  });
3025
+ if (values.env) {
3026
+ if (positionals.length > 1) {
3027
+ error("Too many positional arguments for --env. Use --config <path> or pass a single config path.");
3028
+ }
3029
+ await handleEnvit({
3030
+ directory: values.directory,
3031
+ configPath: values.config || positionals[0],
3032
+ shell: values.shell
3033
+ });
3034
+ return;
3035
+ }
3036
+ if (positionals.length > 1 && !values.command && !isActionInvocation(values)) {
3037
+ const autoName = values.name || generateAutoProcessName();
3038
+ const implicitCommand = joinCommandArgs(positionals);
3039
+ const directory = values.directory || process.cwd();
3040
+ await handleRun({
3041
+ action: "run",
3042
+ name: autoName,
3043
+ command: implicitCommand,
3044
+ directory,
3045
+ configPath: values.config,
3046
+ force: values.force,
3047
+ fetch: values.fetch,
3048
+ remoteName: "",
3049
+ dbPath: values.db,
3050
+ stdout: values.stdout,
3051
+ stderr: values.stderr
3052
+ });
3053
+ return;
3054
+ }
2457
3055
  if (values["_serve"]) {
2458
3056
  redirectConsoleToFiles();
2459
- const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
3057
+ const { startServer: startServer2 } = await init_server().then(() => exports_server);
2460
3058
  await startServer2();
2461
3059
  return;
2462
3060
  }
2463
- if (values["_guard-loop"]) {
3061
+ if (values["_watch-process"]) {
2464
3062
  redirectConsoleToFiles();
2465
- const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
2466
- const intervalStr = positionals[0];
2467
- const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
2468
- await startGuardLoop2(intervalMs);
3063
+ await startProcessWatcher(String(values["_watch-process"]));
2469
3064
  return;
2470
3065
  }
2471
3066
  if (values.dashboard) {
2472
3067
  const dashboardName = "bgr-dashboard";
2473
3068
  const homePath3 = getHomeDir();
2474
- const bgrDir2 = join4(homePath3, ".bgr");
3069
+ const bgrDir2 = join6(homePath3, ".bgr");
2475
3070
  const requestedPort = values.port;
3071
+ const explicitPortValue = requestedPort || Bun.env.BUN_PORT || undefined;
3072
+ const explicitPort = explicitPortValue ? parseInt(explicitPortValue, 10) : null;
2476
3073
  const existing = getProcess(dashboardName);
2477
3074
  if (existing && await isProcessRunning(existing.pid)) {
2478
- let existingPorts = await getProcessPorts(existing.pid);
3075
+ const resolved = await resolvePidWithPorts(existing.pid);
3076
+ let existingPid = resolved.pid;
3077
+ let existingPorts = resolved.ports;
2479
3078
  if (existingPorts.length === 0) {
2480
- const childPid = await findChildPid(existing.pid);
2481
- if (childPid !== existing.pid) {
2482
- existingPorts = await getProcessPorts(childPid);
3079
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3080
+ if (detachedPid && detachedPid !== existingPid) {
3081
+ const detachedResolved = await resolvePidWithPorts(detachedPid);
3082
+ existingPid = detachedResolved.pid;
3083
+ existingPorts = detachedResolved.ports;
2483
3084
  }
2484
3085
  }
3086
+ if (existingPid !== existing.pid && existingPorts.length > 0) {
3087
+ await retryDatabaseOperation(() => updateProcessPid(dashboardName, existingPid));
3088
+ }
2485
3089
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
2486
- announce(`Dashboard is already running (PID ${existing.pid})
3090
+ announce(`Dashboard is already running (PID ${existingPid})
2487
3091
 
2488
3092
  ` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
2489
3093
 
@@ -2502,29 +3106,28 @@ async function run2() {
2502
3106
  }
2503
3107
  await retryDatabaseOperation(() => removeProcessByName(dashboardName));
2504
3108
  }
2505
- const { resolve } = __require("path");
2506
- const scriptPath = resolve(process.argv[1]);
2507
- const spawnCommand = `bun run ${scriptPath} --_serve`;
2508
- const command = `bgrun --_serve`;
2509
- const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
2510
- const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
3109
+ const spawnCommand = `bunx bgrun --_serve`;
3110
+ const command = `bunx bgrun --_serve`;
3111
+ const stdoutPath = join6(bgrDir2, `${dashboardName}-out.txt`);
3112
+ const stderrPath = join6(bgrDir2, `${dashboardName}-err.txt`);
2511
3113
  await Bun.write(stdoutPath, "");
2512
3114
  await Bun.write(stderrPath, "");
2513
3115
  const spawnEnv = { ...Bun.env };
2514
- if (requestedPort) {
2515
- spawnEnv.BUN_PORT = requestedPort;
3116
+ if (explicitPortValue) {
3117
+ spawnEnv.BUN_PORT = explicitPortValue;
3118
+ } else {
3119
+ delete spawnEnv.BUN_PORT;
2516
3120
  }
2517
3121
  spawnEnv.BGR_STDOUT = stdoutPath;
2518
3122
  spawnEnv.BGR_STDERR = stderrPath;
2519
- const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2520
- if (!isNaN(targetPort) && targetPort > 0) {
2521
- const portFree = await isPortFree(targetPort);
3123
+ if (explicitPort && explicitPort > 0) {
3124
+ const portFree = await isPortFree(explicitPort);
2522
3125
  if (!portFree) {
2523
- console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
2524
- await killProcessOnPort(targetPort);
2525
- const freed = await waitForPortFree(targetPort, 5000);
3126
+ console.log(chalk7.yellow(` \u26A1 Requested dashboard port ${explicitPort} is occupied \u2014 reclaiming...`));
3127
+ await killProcessOnPort(explicitPort);
3128
+ const freed = await waitForPortFree(explicitPort, 5000);
2526
3129
  if (!freed) {
2527
- console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
3130
+ console.log(chalk7.red(` \u26A0 Could not free port ${explicitPort} \u2014 dashboard may pick a fallback port`));
2528
3131
  }
2529
3132
  }
2530
3133
  }
@@ -2537,15 +3140,24 @@ async function run2() {
2537
3140
  });
2538
3141
  newProcess.unref();
2539
3142
  await sleep3(2000);
2540
- const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2541
- const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
3143
+ let actualPid = explicitPort && explicitPort > 0 ? await findPidByPort(explicitPort, 1e4) ?? await findChildPid(newProcess.pid) : await findChildPid(newProcess.pid);
3144
+ if (!await isProcessRunning(actualPid)) {
3145
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3146
+ if (detachedPid)
3147
+ actualPid = detachedPid;
3148
+ }
2542
3149
  let actualPort = null;
2543
3150
  for (let attempt = 0;attempt < 10; attempt++) {
2544
- const ports = await getProcessPorts(actualPid);
2545
- if (ports.length > 0) {
2546
- actualPort = ports[0];
3151
+ const resolved = await resolvePidWithPorts(actualPid);
3152
+ actualPid = resolved.pid;
3153
+ if (resolved.ports.length > 0) {
3154
+ actualPort = resolved.ports[0];
2547
3155
  break;
2548
3156
  }
3157
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3158
+ if (detachedPid && detachedPid !== actualPid) {
3159
+ actualPid = detachedPid;
3160
+ }
2549
3161
  await sleep3(1000);
2550
3162
  }
2551
3163
  await retryDatabaseOperation(() => insertProcess({
@@ -2578,75 +3190,8 @@ async function run2() {
2578
3190
  announce(msg, "BGR Dashboard");
2579
3191
  return;
2580
3192
  }
2581
- if (values.guard) {
2582
- const guardName = "bgr-guard";
2583
- const homePath3 = getHomeDir();
2584
- const bgrDir2 = join4(homePath3, ".bgr");
2585
- const existing = getProcess(guardName);
2586
- if (existing && await isProcessRunning(existing.pid)) {
2587
- announce(`Guard is already running (PID ${existing.pid})
2588
-
2589
- Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
2590
- Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2591
- return;
2592
- }
2593
- if (existing) {
2594
- if (await isProcessRunning(existing.pid)) {
2595
- await terminateProcess(existing.pid);
2596
- }
2597
- await retryDatabaseOperation(() => removeProcessByName(guardName));
2598
- }
2599
- const { resolve } = __require("path");
2600
- const scriptPath = resolve(process.argv[1]);
2601
- const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
2602
- const command = `bgrun --_guard-loop`;
2603
- const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
2604
- const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
2605
- await Bun.write(stdoutPath, "");
2606
- await Bun.write(stderrPath, "");
2607
- const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
2608
- env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
2609
- cwd: bgrDir2,
2610
- stdout: "ignore",
2611
- stderr: "ignore",
2612
- detached: true
2613
- });
2614
- newProcess.unref();
2615
- await sleep3(1000);
2616
- let actualPid = await findChildPid(newProcess.pid);
2617
- if (!await isProcessRunning(actualPid)) {
2618
- const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
2619
- 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);
2620
- const foundPid = parseInt(result.trim());
2621
- if (!isNaN(foundPid) && foundPid > 0)
2622
- actualPid = foundPid;
2623
- }
2624
- await retryDatabaseOperation(() => insertProcess({
2625
- pid: actualPid,
2626
- workdir: bgrDir2,
2627
- command,
2628
- name: guardName,
2629
- env: "BGR_KEEP_ALIVE=false",
2630
- configPath: "",
2631
- stdout_path: stdoutPath,
2632
- stderr_path: stderrPath
2633
- }));
2634
- const msg = dedent`
2635
- ${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2636
- ${chalk7.gray("\u2500".repeat(40))}
2637
-
2638
- Monitors: All processes with BGR_KEEP_ALIVE=true
2639
- Also watches: bgr-dashboard (auto-restart if it dies)
2640
- Check interval: 30 seconds
2641
- Backoff: Exponential after 5 rapid crashes
2642
-
2643
- ${chalk7.gray("\u2500".repeat(40))}
2644
- Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
2645
-
2646
- ${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
2647
- ${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2648
- `;
2649
- announce(msg, "BGR Guard");
3193
+ if (values.guard || values["guard-off"]) {
3194
+ await handleGuardToggle(positionals[0], Boolean(values.guard));
2650
3195
  return;
2651
3196
  }
2652
3197
  if (values.version) {