bgrun 3.12.16 → 3.12.22

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,247 +1149,6 @@ var init_log_rotation = __esm(() => {
1068
1149
  DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
1069
1150
  });
1070
1151
 
1071
- // src/logger.ts
1072
- import boxen from "boxen";
1073
- import chalk from "chalk";
1074
- function announce(message, title) {
1075
- console.log(boxen(message, {
1076
- padding: 1,
1077
- margin: 1,
1078
- borderColor: "green",
1079
- title: title || "bgrun",
1080
- titleAlignment: "center",
1081
- borderStyle: "round"
1082
- }));
1083
- }
1084
- function error(message) {
1085
- const text = message instanceof Error ? message.stack || message.message : String(message);
1086
- console.error(boxen(chalk.red(text), {
1087
- padding: 1,
1088
- margin: 1,
1089
- borderColor: "red",
1090
- title: "Error",
1091
- titleAlignment: "center",
1092
- borderStyle: "double"
1093
- }));
1094
- throw new BgrunError(text);
1095
- }
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
- });
1105
-
1106
- // src/config.ts
1107
- function formatEnvKey(key) {
1108
- return key.toUpperCase().replace(/\./g, "_");
1109
- }
1110
- function flattenConfig(obj, prefix = "") {
1111
- return Object.keys(obj).reduce((acc, key) => {
1112
- const value = obj[key];
1113
- const newPrefix = prefix ? `${prefix}.${key}` : key;
1114
- if (Array.isArray(value)) {
1115
- value.forEach((item, index) => {
1116
- const indexedPrefix = `${newPrefix}.${index}`;
1117
- if (typeof item === "object" && item !== null) {
1118
- Object.assign(acc, flattenConfig(item, indexedPrefix));
1119
- } else {
1120
- acc[formatEnvKey(indexedPrefix)] = String(item);
1121
- }
1122
- });
1123
- } else if (typeof value === "object" && value !== null) {
1124
- Object.assign(acc, flattenConfig(value, newPrefix));
1125
- } else {
1126
- acc[formatEnvKey(newPrefix)] = String(value);
1127
- }
1128
- return acc;
1129
- }, {});
1130
- }
1131
- async function parseConfigFile(configPath) {
1132
- const importPath = `${configPath}?t=${Date.now()}`;
1133
- const parsedConfig = await import(importPath).then((m) => m.default);
1134
- return flattenConfig(parsedConfig);
1135
- }
1136
-
1137
- // src/commands/run.ts
1138
- var {$: $2 } = globalThis.Bun;
1139
- var {sleep: sleep2 } = globalThis.Bun;
1140
- import { join as join3 } from "path";
1141
- 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
- }
1159
- }
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.`);
1167
- }
1168
- await run.measure(`Git fetch "${name}"`, async () => {
1169
- 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
- }
1177
- } catch (err) {
1178
- error(`Failed to pull latest changes: ${err}`);
1179
- }
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);
1189
- }
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
- });
1195
- }
1196
- if (detectedPorts.length > 0) {
1197
- await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1198
- for (const port of detectedPorts) {
1199
- await killProcessOnPort(port);
1200
- }
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);
1206
- }
1207
- }
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;
1218
- }
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();
1225
- }
1226
- if (zombiePids.length > 0) {
1227
- announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1228
- }
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}`);
1268
- }
1269
- } 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
1152
  // src/server.ts
1313
1153
  var exports_server = {};
1314
1154
  __export(exports_server, {
@@ -1344,18 +1184,20 @@ async function cleanupPort(port) {
1344
1184
  async function startServer() {
1345
1185
  const { start } = await import("melina");
1346
1186
  const appDir = path.join(import.meta.dir, "../dashboard/app");
1347
- const requestedPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : 3000;
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;
1348
1191
  _originalPort = requestedPort;
1349
- const resolvedPort = await cleanupPort(requestedPort);
1192
+ const resolvedPort = hasExplicitPort ? await cleanupPort(requestedPort) : requestedPort;
1350
1193
  _currentPort = resolvedPort;
1351
- const needsExplicitPort = process.env.BUN_PORT || resolvedPort !== requestedPort;
1194
+ const needsExplicitPort = hasExplicitPort || resolvedPort !== requestedPort;
1352
1195
  await start({
1353
1196
  appDir,
1354
1197
  defaultTitle: "bgrun Dashboard - Process Manager",
1355
1198
  globalCss: path.join(appDir, "globals.css"),
1356
1199
  ...needsExplicitPort && { port: resolvedPort }
1357
1200
  });
1358
- startGuard();
1359
1201
  const { startLogRotation: startLogRotation2 } = await Promise.resolve().then(() => (init_log_rotation(), exports_log_rotation));
1360
1202
  startLogRotation2(() => getAllProcesses());
1361
1203
  if (resolvedPort !== requestedPort) {
@@ -1384,254 +1226,638 @@ function startStickyPortChecker() {
1384
1226
  } catch {}
1385
1227
  }, CHECK_INTERVAL_MS);
1386
1228
  }
1387
- function startGuard() {
1388
- console.log(`[guard] \u2713 Built-in process guard started (checking every ${GUARD_INTERVAL_MS / 1000}s)`);
1389
- setInterval(async () => {
1390
- try {
1391
- const processes = getAllProcesses();
1392
- if (processes.length === 0)
1393
- return;
1394
- for (const proc of processes) {
1395
- if (GUARD_SKIP_NAMES.has(proc.name))
1396
- continue;
1397
- const env = proc.env ? parseEnvString(proc.env) : {};
1398
- if (env.BGR_KEEP_ALIVE !== "true")
1399
- continue;
1400
- const alive = await isProcessRunning(proc.pid, proc.command);
1401
- if (!alive) {
1402
- const now = Date.now();
1403
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1404
- if (now < nextRestart)
1405
- continue;
1406
- console.log(`[guard] \u26A0 Guarded process "${proc.name}" (PID ${proc.pid}) is dead, restarting...`);
1407
- let success = false;
1408
- try {
1409
- await handleRun({
1410
- action: "run",
1411
- name: proc.name,
1412
- force: true,
1413
- remoteName: ""
1414
- });
1415
- success = true;
1416
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1417
- const newCount = prevCount + 1;
1418
- guardRestartCounts.set(proc.name, newCount);
1419
- try {
1420
- addHistoryEntry(proc.name, "restart", undefined, { by: "guard", count: newCount });
1421
- } catch {}
1422
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: true });
1423
- if (guardEvents.length > 100)
1424
- guardEvents.pop();
1425
- if (newCount > 5) {
1426
- const backoffSeconds = Math.min(30 * Math.pow(2, newCount - 6), 300);
1427
- guardNextRestartTime.set(proc.name, Date.now() + backoffSeconds * 1000);
1428
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount}). Crash loop detected: next check delayed by ${backoffSeconds}s.`);
1429
- } else {
1430
- console.log(`[guard] \u2713 Restarted "${proc.name}" (restart #${newCount})`);
1431
- }
1432
- } catch (err) {
1433
- console.error(`[guard] \u2717 Failed to restart "${proc.name}": ${err.message}`);
1434
- guardEvents.unshift({ time: now, name: proc.name, action: "restart", success: false });
1435
- if (guardEvents.length > 100)
1436
- guardEvents.pop();
1437
- }
1438
- } else {
1439
- const prevCount = guardRestartCounts.get(proc.name) || 0;
1440
- if (prevCount > 0) {
1441
- const nextRestart = guardNextRestartTime.get(proc.name) || 0;
1442
- if (Date.now() > nextRestart + 60000) {
1443
- guardRestartCounts.delete(proc.name);
1444
- guardNextRestartTime.delete(proc.name);
1445
- }
1446
- }
1447
- }
1448
- }
1449
- } catch (err) {
1450
- console.error(`[guard] Error in guard loop: ${err.message}`);
1451
- }
1452
- }, GUARD_INTERVAL_MS);
1453
- }
1454
- var GUARD_INTERVAL_MS = 30000, GUARD_SKIP_NAMES, _g, guardRestartCounts, guardNextRestartTime, guardEvents, _originalPort = 3000, _currentPort = 3000;
1455
- var init_server = __esm(() => {
1229
+ var guardRestartCounts, guardEvents, _originalPort = 3000, _currentPort = 3000;
1230
+ var init_server = __esm(async () => {
1456
1231
  init_db();
1457
1232
  init_platform();
1458
- init_run();
1459
- init_utils();
1460
- GUARD_SKIP_NAMES = new Set(["bgr-dashboard", "bgr-guard"]);
1461
- _g = globalThis;
1462
- if (!_g.__bgrGuardRestartCounts)
1463
- _g.__bgrGuardRestartCounts = new Map;
1464
- if (!_g.__bgrGuardNextRestartTime)
1465
- _g.__bgrGuardNextRestartTime = new Map;
1466
- if (!_g.__bgrGuardEvents)
1467
- _g.__bgrGuardEvents = [];
1468
- guardRestartCounts = _g.__bgrGuardRestartCounts;
1469
- guardNextRestartTime = _g.__bgrGuardNextRestartTime;
1470
- guardEvents = _g.__bgrGuardEvents;
1233
+ guardRestartCounts = new Map;
1234
+ guardEvents = [];
1235
+ if (import.meta.main) {
1236
+ await startServer();
1237
+ }
1471
1238
  });
1472
1239
 
1473
- // src/guard.ts
1474
- var exports_guard = {};
1475
- __export(exports_guard, {
1476
- startGuardLoop: () => startGuardLoop
1477
- });
1478
- import { createHmac } from "crypto";
1479
- async function notifyWebhook(event, name, details) {
1480
- if (!WEBHOOK_URL)
1481
- return;
1482
- try {
1483
- const payload = JSON.stringify({
1484
- event,
1485
- process: name,
1486
- timestamp: new Date().toISOString(),
1487
- ...details
1488
- });
1489
- const headers = {
1490
- "Content-Type": "application/json",
1491
- "User-Agent": "bgrun-guard/1.0"
1492
- };
1493
- if (WEBHOOK_SECRET) {
1494
- const sig = createHmac("sha256", WEBHOOK_SECRET).update(payload).digest("hex");
1495
- headers["X-BGR-Signature"] = `sha256=${sig}`;
1496
- }
1497
- const controller = new AbortController;
1498
- const timeout = setTimeout(() => controller.abort(), 5000);
1499
- await fetch(WEBHOOK_URL, {
1500
- method: "POST",
1501
- headers,
1502
- body: payload,
1503
- signal: controller.signal
1504
- });
1505
- clearTimeout(timeout);
1506
- } catch (err) {
1507
- console.error(`[guard] Webhook failed: ${err.message}`);
1508
- }
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
+
1248
+ // src/logger.ts
1249
+ import boxen from "boxen";
1250
+ import chalk from "chalk";
1251
+ function announce(message, title) {
1252
+ console.log(boxen(message, {
1253
+ padding: 1,
1254
+ margin: 1,
1255
+ borderColor: "green",
1256
+ title: title || "bgrun",
1257
+ titleAlignment: "center",
1258
+ borderStyle: "round"
1259
+ }));
1509
1260
  }
1510
- async function restartProcess(name) {
1511
- try {
1512
- await handleRun({
1513
- action: "run",
1514
- name,
1515
- force: true,
1516
- remoteName: ""
1517
- });
1518
- return true;
1519
- } catch (err) {
1520
- console.error(`[guard] \u2717 Failed to restart "${name}": ${err.message}`);
1521
- return false;
1261
+
1262
+ class BgrunError extends Error {
1263
+ constructor(message) {
1264
+ super(message);
1265
+ this.name = "BgrunError";
1522
1266
  }
1523
1267
  }
1524
- function getBackoffMs(restartCount) {
1525
- if (restartCount <= CRASH_THRESHOLD)
1526
- return 0;
1527
- const exponent = restartCount - CRASH_THRESHOLD;
1528
- return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
1529
- }
1530
- async function guardCycle() {
1268
+ function error(message) {
1269
+ const text = message instanceof Error ? message.stack || message.message : String(message);
1270
+ console.error(boxen(chalk.red(text), {
1271
+ padding: 1,
1272
+ margin: 1,
1273
+ borderColor: "red",
1274
+ title: "Error",
1275
+ titleAlignment: "center",
1276
+ borderStyle: "double"
1277
+ }));
1278
+ throw new BgrunError(text);
1279
+ }
1280
+
1281
+ // src/commands/run.ts
1282
+ init_utils();
1283
+
1284
+ // src/config.ts
1285
+ function formatEnvKey(key) {
1286
+ return key.toUpperCase().replace(/\./g, "_");
1287
+ }
1288
+ function flattenConfig(obj, prefix = "") {
1289
+ return Object.keys(obj).reduce((acc, key) => {
1290
+ const value = obj[key];
1291
+ const newPrefix = prefix ? `${prefix}.${key}` : key;
1292
+ if (Array.isArray(value)) {
1293
+ value.forEach((item, index) => {
1294
+ const indexedPrefix = `${newPrefix}.${index}`;
1295
+ if (typeof item === "object" && item !== null) {
1296
+ Object.assign(acc, flattenConfig(item, indexedPrefix));
1297
+ } else {
1298
+ acc[formatEnvKey(indexedPrefix)] = String(item);
1299
+ }
1300
+ });
1301
+ } else if (typeof value === "object" && value !== null) {
1302
+ Object.assign(acc, flattenConfig(value, newPrefix));
1303
+ } else {
1304
+ acc[formatEnvKey(newPrefix)] = String(value);
1305
+ }
1306
+ return acc;
1307
+ }, {});
1308
+ }
1309
+ async function parseConfigFile(configPath) {
1310
+ const importPath = `${configPath}?t=${Date.now()}`;
1311
+ const parsedConfig = await import(importPath).then((m) => m.default);
1312
+ return flattenConfig(parsedConfig);
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
+ }
1330
+
1331
+ // src/commands/run.ts
1332
+ var {$: $2 } = globalThis.Bun;
1333
+ var {sleep: sleep2 } = globalThis.Bun;
1334
+ import { join as join5 } from "path";
1335
+ import { createMeasure as createMeasure2 } from "measure-fn";
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;
1501
+ }
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;
1531
1548
  try {
1532
- const processes = getAllProcesses();
1533
- if (processes.length === 0)
1534
- return;
1535
- const now = Date.now();
1536
- let checked = 0;
1537
- let restarted = 0;
1538
- let skipped = 0;
1539
- for (const proc of processes) {
1540
- if (proc.name === "bgr-guard")
1541
- continue;
1542
- const env = proc.env ? parseEnvString(proc.env) : {};
1543
- const isGuarded = env.BGR_KEEP_ALIVE === "true";
1544
- const isDashboard = proc.name === "bgr-dashboard";
1545
- if (!isGuarded && !isDashboard)
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;
1560
+ }
1561
+ if (proc.pid <= 0 || isProcessOperationLocked(targetName)) {
1562
+ await Bun.sleep(intervalMs);
1546
1563
  continue;
1547
- checked++;
1548
- try {
1549
- const alive = await isProcessRunning(proc.pid, proc.command);
1550
- if (!alive && proc.pid > 0) {
1551
- const nextRestart = state.nextRestartTime.get(proc.name) || 0;
1552
- if (now < nextRestart) {
1553
- const waitSecs = Math.round((nextRestart - now) / 1000);
1554
- skipped++;
1555
- 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
+ }
1572
+ try {
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 });
1585
+ } catch (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}`);
1588
+ }
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);
1600
+ }
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
+ }
1556
1669
  }
1557
- console.log(`[guard] \u26A0 "${proc.name}" (PID ${proc.pid}) is dead \u2014 restarting...`);
1558
- notifyWebhook("crash", proc.name, { pid: proc.pid, isDashboard });
1559
- const success = await restartProcess(proc.name);
1560
- if (success) {
1561
- const count = (state.restartCounts.get(proc.name) || 0) + 1;
1562
- state.restartCounts.set(proc.name, count);
1563
- state.lastSeenAlive.delete(proc.name);
1564
- const backoff = getBackoffMs(count);
1565
- if (backoff > 0) {
1566
- state.nextRestartTime.set(proc.name, now + backoff);
1567
- console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count}). Crash loop: next check in ${Math.round(backoff / 1000)}s`);
1568
- } else {
1569
- console.log(`[guard] \u2713 Restarted "${proc.name}" (#${count})`);
1670
+ });
1671
+ }
1672
+ }
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.`);
1680
+ }
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");
1570
1689
  }
1571
- restarted++;
1572
- notifyWebhook("restart", proc.name, { pid: proc.pid, restartCount: count, backoffMs: backoff });
1573
- } else {
1574
- notifyWebhook("restart_failed", proc.name, { pid: proc.pid });
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
1575
1707
  }
1576
- } else if (alive) {
1577
- const count = state.restartCounts.get(proc.name) || 0;
1578
- if (count > 0) {
1579
- const lastSeen = state.lastSeenAlive.get(proc.name);
1580
- if (!lastSeen) {
1581
- state.lastSeenAlive.set(proc.name, now);
1582
- } else if (now - lastSeen > STABILITY_WINDOW_MS) {
1583
- state.restartCounts.delete(proc.name);
1584
- state.nextRestartTime.delete(proc.name);
1585
- state.lastSeenAlive.delete(proc.name);
1586
- console.log(`[guard] \u2713 "${proc.name}" stable for ${Math.round(STABILITY_WINDOW_MS / 1000)}s \u2014 reset counters`);
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);
1714
+ }
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);
1731
+ }
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);
1587
1737
  }
1588
1738
  }
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
+ }
1755
+ }
1756
+ });
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));
1788
+ } else {
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;
1804
+ } else {
1805
+ finalConfigPath = ".config.toml";
1806
+ }
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 () => {
1811
+ try {
1812
+ return await parseConfigFile(fullConfigPath);
1813
+ } catch (err) {
1814
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1815
+ return null;
1816
+ }
1817
+ });
1818
+ if (configEnv) {
1819
+ finalEnv = { ...finalEnv, ...configEnv };
1820
+ console.log(`Loaded config from ${finalConfigPath}`);
1589
1821
  }
1590
- } catch (err) {
1591
- console.error(`[guard] Error checking "${proc.name}": ${err.message}`);
1822
+ } else {
1823
+ console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1592
1824
  }
1593
1825
  }
1594
- if (restarted > 0) {
1595
- console.log(`[guard] Cycle: ${checked} checked, ${restarted} restarted, ${skipped} in backoff`);
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,
1847
+ name,
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);
1596
1855
  }
1597
- } catch (err) {
1598
- 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();
1599
1859
  }
1600
1860
  }
1601
- async function startGuardLoop(intervalMs = DEFAULT_INTERVAL_MS) {
1602
- const interval = intervalMs || DEFAULT_INTERVAL_MS;
1603
- 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`);
1604
- console.log(`[guard] \uD83D\uDEE1\uFE0F BGR Standalone Guard started`);
1605
- console.log(`[guard] Check interval: ${interval / 1000}s`);
1606
- console.log(`[guard] Crash backoff threshold: ${CRASH_THRESHOLD} restarts`);
1607
- console.log(`[guard] Stability window: ${STABILITY_WINDOW_MS / 1000}s`);
1608
- console.log(`[guard] Monitoring: BGR_KEEP_ALIVE=true + bgr-dashboard`);
1609
- console.log(`[guard] Webhook: ${WEBHOOK_URL || "(none \u2014 set BGR_WEBHOOK_URL to enable)"}`);
1610
- console.log(`[guard] Started: ${new Date().toLocaleString()}`);
1611
- 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`);
1612
- await guardCycle();
1613
- setInterval(guardCycle, interval);
1614
- }
1615
- var WEBHOOK_URL, WEBHOOK_SECRET, DEFAULT_INTERVAL_MS = 30000, MAX_BACKOFF_MS, CRASH_THRESHOLD = 5, STABILITY_WINDOW_MS = 120000, state;
1616
- var init_guard = __esm(() => {
1617
- init_db();
1618
- init_platform();
1619
- init_run();
1620
- init_utils();
1621
- WEBHOOK_URL = process.env.BGR_WEBHOOK_URL || "";
1622
- WEBHOOK_SECRET = process.env.BGR_WEBHOOK_SECRET || "";
1623
- MAX_BACKOFF_MS = 5 * 60000;
1624
- state = {
1625
- restartCounts: new Map,
1626
- nextRestartTime: new Map,
1627
- lastSeenAlive: new Map
1628
- };
1629
- });
1630
-
1631
- // src/index.ts
1632
- init_utils();
1633
- init_run();
1634
- import { parseArgs } from "util";
1635
1861
 
1636
1862
  // src/commands/list.ts
1637
1863
  import chalk3 from "chalk";
@@ -1801,7 +2027,6 @@ function renderProcessTable(processes, options) {
1801
2027
 
1802
2028
  // src/commands/list.ts
1803
2029
  init_db();
1804
- init_logger();
1805
2030
  init_utils();
1806
2031
  init_platform();
1807
2032
  function formatMemory(bytes) {
@@ -1815,6 +2040,8 @@ function formatMemory(bytes) {
1815
2040
  async function showAll(opts) {
1816
2041
  const processes = getAllProcesses();
1817
2042
  const filtered = processes.filter((proc) => {
2043
+ if (isInternalProcessName(proc.name))
2044
+ return false;
1818
2045
  if (!opts?.filter)
1819
2046
  return true;
1820
2047
  const envVars = parseEnvString(proc.env);
@@ -1862,11 +2089,21 @@ async function showAll(opts) {
1862
2089
  for (const proc of filtered) {
1863
2090
  const isRunning = aliveCache.get(proc.pid) ?? await isProcessRunning(proc.pid, proc.command);
1864
2091
  const runtime = calculateRuntime(proc.timestamp);
1865
- const mem = isRunning ? resourceMap.get(proc.pid)?.memory || 0 : 0;
1866
- 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;
1867
2104
  tableData.push({
1868
2105
  id: proc.id,
1869
- pid: proc.pid,
2106
+ pid: displayPid,
1870
2107
  name: proc.name,
1871
2108
  port: ports.length > 0 ? ports.map((p) => `:${p}`).join(",") : "-",
1872
2109
  memory: formatMemory(mem),
@@ -1898,7 +2135,7 @@ async function showAll(opts) {
1898
2135
  // src/commands/cleanup.ts
1899
2136
  init_db();
1900
2137
  init_platform();
1901
- init_logger();
2138
+ init_utils();
1902
2139
  import * as fs3 from "fs";
1903
2140
  async function handleDelete(name) {
1904
2141
  const process2 = getProcess(name);
@@ -1910,6 +2147,9 @@ async function handleDelete(name) {
1910
2147
  if (isRunning) {
1911
2148
  await terminateProcess(process2.pid);
1912
2149
  }
2150
+ if (!isInternalProcessName(name)) {
2151
+ await stopProcessWatcher(name);
2152
+ }
1913
2153
  if (fs3.existsSync(process2.stdout_path)) {
1914
2154
  try {
1915
2155
  fs3.unlinkSync(process2.stdout_path);
@@ -1930,6 +2170,12 @@ async function handleClean() {
1930
2170
  for (const proc of processes) {
1931
2171
  const running = await isProcessRunning(proc.pid);
1932
2172
  if (!running) {
2173
+ const watched = getWatchedProcessName(proc.name);
2174
+ if (watched) {
2175
+ removeProcess(proc.pid);
2176
+ cleanedCount++;
2177
+ continue;
2178
+ }
1933
2179
  removeProcess(proc.pid);
1934
2180
  cleanedCount++;
1935
2181
  if (fs3.existsSync(proc.stdout_path)) {
@@ -1958,18 +2204,33 @@ async function handleStop(name) {
1958
2204
  error(`No process found named '${name}'`);
1959
2205
  return;
1960
2206
  }
1961
- const isRunning = await isProcessRunning(proc.pid);
1962
- if (!isRunning) {
1963
- announce(`Process '${name}' is already stopped.`, "Process Stop");
1964
- return;
1965
- }
1966
- const ports = await getProcessPorts(proc.pid);
1967
- await terminateProcess(proc.pid);
1968
- for (const port of ports) {
1969
- 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();
1970
2233
  }
1971
- updateProcessPid(name, 0);
1972
- announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
1973
2234
  }
1974
2235
  async function handleDeleteAll() {
1975
2236
  const processes = getAllProcesses();
@@ -1980,6 +2241,9 @@ async function handleDeleteAll() {
1980
2241
  let killedCount = 0;
1981
2242
  let portsFreed = 0;
1982
2243
  for (const proc of processes) {
2244
+ if (!isInternalProcessName(proc.name)) {
2245
+ await stopProcessWatcher(proc.name);
2246
+ }
1983
2247
  const running = await isProcessRunning(proc.pid);
1984
2248
  if (running) {
1985
2249
  const ports = await getProcessPorts(proc.pid);
@@ -1995,6 +2259,18 @@ async function handleDeleteAll() {
1995
2259
  portsFreed++;
1996
2260
  }
1997
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
+ }
1998
2274
  if (fs3.existsSync(proc.stdout_path)) {
1999
2275
  try {
2000
2276
  fs3.unlinkSync(proc.stdout_path);
@@ -2018,9 +2294,7 @@ async function handleDeleteAll() {
2018
2294
  // src/commands/watch.ts
2019
2295
  init_db();
2020
2296
  init_platform();
2021
- init_logger();
2022
2297
  init_utils();
2023
- init_run();
2024
2298
  import * as fs4 from "fs";
2025
2299
  import path2 from "path";
2026
2300
  import chalk4 from "chalk";
@@ -2210,7 +2484,6 @@ SIGINT received...`));
2210
2484
 
2211
2485
  // src/commands/logs.ts
2212
2486
  init_db();
2213
- init_logger();
2214
2487
  init_platform();
2215
2488
  import chalk5 from "chalk";
2216
2489
  import * as fs5 from "fs";
@@ -2255,7 +2528,6 @@ async function showLogs(name, logType = "both", lines) {
2255
2528
  }
2256
2529
 
2257
2530
  // src/commands/details.ts
2258
- init_logger();
2259
2531
  init_db();
2260
2532
  init_utils();
2261
2533
  init_platform();
@@ -2266,6 +2538,10 @@ async function showDetails(name) {
2266
2538
  error(`No process found named '${name}'`);
2267
2539
  return;
2268
2540
  }
2541
+ if (isInternalProcessName(proc.name)) {
2542
+ error(`'${name}' is an internal bgrun process.`);
2543
+ return;
2544
+ }
2269
2545
  let isRunning = await isProcessRunning(proc.pid, proc.command);
2270
2546
  if (!isRunning && proc.pid > 0) {
2271
2547
  const reconciled = await reconcileProcessPids([{ name: proc.name, pid: proc.pid, command: proc.command, workdir: proc.workdir }], new Set([proc.pid]));
@@ -2278,7 +2554,15 @@ async function showDetails(name) {
2278
2554
  }
2279
2555
  const runtime = calculateRuntime(proc.timestamp);
2280
2556
  const envVars = parseEnvString(proc.env);
2281
- 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
+ }
2282
2566
  const portDisplay = ports.length > 0 ? ports.map((p) => chalk6.hex("#FF6B6B")(`:${p}`)).join(", ") : null;
2283
2567
  const details = `
2284
2568
  ${chalk6.bold("Process Details:")}
@@ -2302,13 +2586,225 @@ ${Object.entries(envVars).map(([key, value]) => `${chalk6.cyan.bold(key)} = ${ch
2302
2586
  announce(details, `Process Details: ${name}`);
2303
2587
  }
2304
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
+
2305
2802
  // src/index.ts
2306
- init_logger();
2307
2803
  init_platform();
2308
2804
  init_db();
2309
2805
  import dedent from "dedent";
2310
2806
  import chalk7 from "chalk";
2311
- import { join as join4 } from "path";
2807
+ import { join as join6 } from "path";
2312
2808
  var {sleep: sleep3 } = globalThis.Bun;
2313
2809
  import { configure } from "measure-fn";
2314
2810
  if (!Bun.argv.includes("--_serve")) {
@@ -2356,32 +2852,50 @@ function redirectConsoleToFiles() {
2356
2852
  };
2357
2853
  }
2358
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
+ }
2359
2866
  async function showHelp() {
2360
2867
  const usage = dedent`
2361
2868
  ${chalk7.bold("bgrun \u2014 Bun Background Runner")}
2362
2869
  ${chalk7.gray("\u2550".repeat(50))}
2363
2870
 
2364
2871
  ${chalk7.yellow("Usage:")}
2365
- bgrun [name] [options]
2872
+ bunx bgrun [name] [options]
2366
2873
 
2367
2874
  ${chalk7.yellow("Commands:")}
2368
- bgrun List all processes
2369
- bgrun [name] Show details for a process
2370
- bgrun --dashboard Launch web dashboard (managed by bgrun)
2371
- bgrun --guard Launch standalone process guard
2372
- bgrun --restart [name] Restart a process
2373
- bgrun --restart-all Restart ALL registered processes
2374
- bgrun --stop [name] Stop a process (keep in registry)
2375
- bgrun --stop-all Stop ALL running processes
2376
- bgrun --delete [name] Delete a process
2377
- bgrun --clean Remove all stopped processes
2378
- 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
2379
2890
 
2380
2891
  ${chalk7.yellow("Options:")}
2381
2892
  --name <string> Process name (required for new)
2382
2893
  --command <string> Process command (required for new)
2383
2894
  --directory <path> Working directory (required for new)
2384
2895
  --config <path> Config file (default: .config.toml)
2896
+ --no-config Disable automatic .config.toml loading
2897
+ --env Print shell export commands from config and exit
2898
+ --shell <type> Shell for --env: powershell | cmd | sh | json
2385
2899
  --watch Watch for file changes and auto-restart
2386
2900
  --force Force restart existing process
2387
2901
  --fetch Fetch latest git changes before running
@@ -2394,85 +2908,189 @@ async function showHelp() {
2394
2908
  --version Show version
2395
2909
  --debug Show debug info (DB path, BGR home, etc.)
2396
2910
  --dashboard Launch web dashboard as bgrun-managed process
2911
+ --guard Enable per-process crash watcher
2912
+ --guard-off Disable per-process crash watcher
2397
2913
  --port <number> Port for dashboard (default: 3000)
2398
2914
  --help Show this help message
2399
2915
 
2400
2916
  ${chalk7.yellow("Examples:")}
2401
- bgrun --dashboard
2402
- bgrun --name myapp --command "bun run dev" --directory . --watch
2403
- bgrun myapp --logs --lines 50
2917
+ bunx bgrun -- bun run dev
2918
+ bunx bgrun --no-config -- bun run script.ts
2919
+ bunx bgrun --force -- bun run server.ts
2920
+ bunx bgrun inline -- bun run dev
2921
+ Invoke-Expression (bunx bgrun --env)
2922
+ eval "$(bunx bgrun --env --shell sh)"
2923
+ bunx bgrun --dashboard
2924
+ bunx bgrun myapp --guard
2925
+ bunx bgrun myapp --guard-off
2926
+ bunx bgrun --name myapp --command "bun run dev" --directory . --watch
2927
+ bunx bgrun myapp --logs --lines 50
2404
2928
  `;
2405
2929
  console.log(usage);
2406
2930
  }
2931
+ var cliArgOptions = {
2932
+ name: { type: "string" },
2933
+ command: { type: "string" },
2934
+ directory: { type: "string" },
2935
+ config: { type: "string" },
2936
+ "no-config": { type: "boolean" },
2937
+ env: { type: "boolean" },
2938
+ shell: { type: "string" },
2939
+ watch: { type: "boolean" },
2940
+ force: { type: "boolean" },
2941
+ fetch: { type: "boolean" },
2942
+ delete: { type: "boolean" },
2943
+ nuke: { type: "boolean" },
2944
+ restart: { type: "boolean" },
2945
+ "restart-all": { type: "boolean" },
2946
+ stop: { type: "boolean" },
2947
+ "stop-all": { type: "boolean" },
2948
+ clean: { type: "boolean" },
2949
+ json: { type: "boolean" },
2950
+ logs: { type: "boolean" },
2951
+ "log-stdout": { type: "boolean" },
2952
+ "log-stderr": { type: "boolean" },
2953
+ lines: { type: "string" },
2954
+ filter: { type: "string" },
2955
+ version: { type: "boolean" },
2956
+ help: { type: "boolean" },
2957
+ db: { type: "string" },
2958
+ stdout: { type: "string" },
2959
+ stderr: { type: "string" },
2960
+ dashboard: { type: "boolean" },
2961
+ guard: { type: "boolean" },
2962
+ "guard-off": { type: "boolean" },
2963
+ debug: { type: "boolean" },
2964
+ _serve: { type: "boolean" },
2965
+ "_watch-process": { type: "string" },
2966
+ port: { type: "string" }
2967
+ };
2407
2968
  async function run2() {
2969
+ const rawArgs = Bun.argv.slice(2);
2970
+ const isActionInvocation = (values2) => {
2971
+ return Boolean(values2.dashboard || values2.guard || values2["guard-off"] || values2.version || values2.help || values2.debug || values2.nuke || values2.clean || values2["restart-all"] || values2["stop-all"] || values2.delete || values2.restart || values2.stop || values2.logs || values2["log-stdout"] || values2["log-stderr"] || values2.watch || values2.json || values2.filter);
2972
+ };
2973
+ if (rawArgs[0] === "inline") {
2974
+ const parsed = parseInlineArgs(rawArgs.slice(1));
2975
+ if (parsed.help) {
2976
+ console.log(dedent`
2977
+ ${chalk7.bold("bgrun inline")}
2978
+ ${chalk7.gray("\u2500".repeat(40))}
2979
+
2980
+ Run a command in the current terminal with env vars loaded from a bgrun config file.
2981
+
2982
+ Usage:
2983
+ bunx bgrun inline [--directory <path>] [--config <path>] -- <command> [args...]
2984
+
2985
+ Examples:
2986
+ bunx bgrun inline -- bun run dev
2987
+ bunx bgrun inline --directory apps/api -- node server.js
2988
+ `);
2989
+ return;
2990
+ }
2991
+ await handleInline(parsed);
2992
+ return;
2993
+ }
2994
+ const delimiterIndex = rawArgs.indexOf("--");
2995
+ if (delimiterIndex !== -1 && delimiterIndex < rawArgs.length - 1) {
2996
+ const preArgs = rawArgs.slice(0, delimiterIndex);
2997
+ const commandArgs = rawArgs.slice(delimiterIndex + 1);
2998
+ const { values: values2 } = parseArgs({
2999
+ args: preArgs,
3000
+ options: cliArgOptions,
3001
+ strict: false,
3002
+ allowPositionals: true
3003
+ });
3004
+ const autoName = values2.name || generateAutoProcessName();
3005
+ const inlineCommand = joinCommandArgs(commandArgs);
3006
+ const directory = values2.directory || process.cwd();
3007
+ await handleRun({
3008
+ action: "run",
3009
+ name: autoName,
3010
+ command: inlineCommand,
3011
+ directory,
3012
+ configPath: values2["no-config"] ? "" : values2.config,
3013
+ force: values2.force,
3014
+ fetch: values2.fetch,
3015
+ remoteName: "",
3016
+ dbPath: values2.db,
3017
+ stdout: values2.stdout,
3018
+ stderr: values2.stderr
3019
+ });
3020
+ return;
3021
+ }
2408
3022
  const { values, positionals } = parseArgs({
2409
- args: Bun.argv.slice(2),
2410
- options: {
2411
- name: { type: "string" },
2412
- command: { type: "string" },
2413
- directory: { type: "string" },
2414
- config: { type: "string" },
2415
- watch: { type: "boolean" },
2416
- force: { type: "boolean" },
2417
- fetch: { type: "boolean" },
2418
- delete: { type: "boolean" },
2419
- nuke: { type: "boolean" },
2420
- restart: { type: "boolean" },
2421
- "restart-all": { type: "boolean" },
2422
- stop: { type: "boolean" },
2423
- "stop-all": { type: "boolean" },
2424
- clean: { type: "boolean" },
2425
- json: { type: "boolean" },
2426
- logs: { type: "boolean" },
2427
- "log-stdout": { type: "boolean" },
2428
- "log-stderr": { type: "boolean" },
2429
- lines: { type: "string" },
2430
- filter: { type: "string" },
2431
- version: { type: "boolean" },
2432
- help: { type: "boolean" },
2433
- db: { type: "string" },
2434
- stdout: { type: "string" },
2435
- stderr: { type: "string" },
2436
- dashboard: { type: "boolean" },
2437
- guard: { type: "boolean" },
2438
- debug: { type: "boolean" },
2439
- _serve: { type: "boolean" },
2440
- "_guard-loop": { type: "boolean" },
2441
- port: { type: "string" }
2442
- },
3023
+ args: rawArgs,
3024
+ options: cliArgOptions,
2443
3025
  strict: false,
2444
3026
  allowPositionals: true
2445
3027
  });
3028
+ if (values.env) {
3029
+ if (positionals.length > 1) {
3030
+ error("Too many positional arguments for --env. Use --config <path> or pass a single config path.");
3031
+ }
3032
+ await handleEnvit({
3033
+ directory: values.directory,
3034
+ configPath: values.config || positionals[0],
3035
+ shell: values.shell
3036
+ });
3037
+ return;
3038
+ }
3039
+ if (positionals.length > 1 && !values.command && !isActionInvocation(values)) {
3040
+ const autoName = values.name || generateAutoProcessName();
3041
+ const implicitCommand = joinCommandArgs(positionals);
3042
+ const directory = values.directory || process.cwd();
3043
+ await handleRun({
3044
+ action: "run",
3045
+ name: autoName,
3046
+ command: implicitCommand,
3047
+ directory,
3048
+ configPath: values["no-config"] ? "" : values.config,
3049
+ force: values.force,
3050
+ fetch: values.fetch,
3051
+ remoteName: "",
3052
+ dbPath: values.db,
3053
+ stdout: values.stdout,
3054
+ stderr: values.stderr
3055
+ });
3056
+ return;
3057
+ }
2446
3058
  if (values["_serve"]) {
2447
3059
  redirectConsoleToFiles();
2448
- const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
3060
+ const { startServer: startServer2 } = await init_server().then(() => exports_server);
2449
3061
  await startServer2();
2450
3062
  return;
2451
3063
  }
2452
- if (values["_guard-loop"]) {
3064
+ if (values["_watch-process"]) {
2453
3065
  redirectConsoleToFiles();
2454
- const { startGuardLoop: startGuardLoop2 } = await Promise.resolve().then(() => (init_guard(), exports_guard));
2455
- const intervalStr = positionals[0];
2456
- const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
2457
- await startGuardLoop2(intervalMs);
3066
+ await startProcessWatcher(String(values["_watch-process"]));
2458
3067
  return;
2459
3068
  }
2460
3069
  if (values.dashboard) {
2461
3070
  const dashboardName = "bgr-dashboard";
2462
3071
  const homePath3 = getHomeDir();
2463
- const bgrDir2 = join4(homePath3, ".bgr");
3072
+ const bgrDir2 = join6(homePath3, ".bgr");
2464
3073
  const requestedPort = values.port;
3074
+ const explicitPortValue = requestedPort || Bun.env.BUN_PORT || undefined;
3075
+ const explicitPort = explicitPortValue ? parseInt(explicitPortValue, 10) : null;
2465
3076
  const existing = getProcess(dashboardName);
2466
3077
  if (existing && await isProcessRunning(existing.pid)) {
2467
- let existingPorts = await getProcessPorts(existing.pid);
3078
+ const resolved = await resolvePidWithPorts(existing.pid);
3079
+ let existingPid = resolved.pid;
3080
+ let existingPorts = resolved.ports;
2468
3081
  if (existingPorts.length === 0) {
2469
- const childPid = await findChildPid(existing.pid);
2470
- if (childPid !== existing.pid) {
2471
- existingPorts = await getProcessPorts(childPid);
3082
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3083
+ if (detachedPid && detachedPid !== existingPid) {
3084
+ const detachedResolved = await resolvePidWithPorts(detachedPid);
3085
+ existingPid = detachedResolved.pid;
3086
+ existingPorts = detachedResolved.ports;
2472
3087
  }
2473
3088
  }
3089
+ if (existingPid !== existing.pid && existingPorts.length > 0) {
3090
+ await retryDatabaseOperation(() => updateProcessPid(dashboardName, existingPid));
3091
+ }
2474
3092
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : "(detecting...)";
2475
- announce(`Dashboard is already running (PID ${existing.pid})
3093
+ announce(`Dashboard is already running (PID ${existingPid})
2476
3094
 
2477
3095
  ` + ` \uD83C\uDF10 ${chalk7.cyan(`http://localhost${portStr}`)}
2478
3096
 
@@ -2491,29 +3109,28 @@ async function run2() {
2491
3109
  }
2492
3110
  await retryDatabaseOperation(() => removeProcessByName(dashboardName));
2493
3111
  }
2494
- const { resolve } = __require("path");
2495
- const scriptPath = resolve(process.argv[1]);
2496
- const spawnCommand = `bun run ${scriptPath} --_serve`;
2497
- const command = `bgrun --_serve`;
2498
- const stdoutPath = join4(bgrDir2, `${dashboardName}-out.txt`);
2499
- const stderrPath = join4(bgrDir2, `${dashboardName}-err.txt`);
3112
+ const spawnCommand = `bunx bgrun --_serve`;
3113
+ const command = `bunx bgrun --_serve`;
3114
+ const stdoutPath = join6(bgrDir2, `${dashboardName}-out.txt`);
3115
+ const stderrPath = join6(bgrDir2, `${dashboardName}-err.txt`);
2500
3116
  await Bun.write(stdoutPath, "");
2501
3117
  await Bun.write(stderrPath, "");
2502
3118
  const spawnEnv = { ...Bun.env };
2503
- if (requestedPort) {
2504
- spawnEnv.BUN_PORT = requestedPort;
3119
+ if (explicitPortValue) {
3120
+ spawnEnv.BUN_PORT = explicitPortValue;
3121
+ } else {
3122
+ delete spawnEnv.BUN_PORT;
2505
3123
  }
2506
3124
  spawnEnv.BGR_STDOUT = stdoutPath;
2507
3125
  spawnEnv.BGR_STDERR = stderrPath;
2508
- const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2509
- if (!isNaN(targetPort) && targetPort > 0) {
2510
- const portFree = await isPortFree(targetPort);
3126
+ if (explicitPort && explicitPort > 0) {
3127
+ const portFree = await isPortFree(explicitPort);
2511
3128
  if (!portFree) {
2512
- console.log(chalk7.yellow(` \u26A1 Port ${targetPort} is occupied \u2014 reclaiming...`));
2513
- await killProcessOnPort(targetPort);
2514
- const freed = await waitForPortFree(targetPort, 5000);
3129
+ console.log(chalk7.yellow(` \u26A1 Requested dashboard port ${explicitPort} is occupied \u2014 reclaiming...`));
3130
+ await killProcessOnPort(explicitPort);
3131
+ const freed = await waitForPortFree(explicitPort, 5000);
2515
3132
  if (!freed) {
2516
- console.log(chalk7.red(` \u26A0 Could not free port ${targetPort} \u2014 dashboard may pick a fallback port`));
3133
+ console.log(chalk7.red(` \u26A0 Could not free port ${explicitPort} \u2014 dashboard may pick a fallback port`));
2517
3134
  }
2518
3135
  }
2519
3136
  }
@@ -2526,15 +3143,24 @@ async function run2() {
2526
3143
  });
2527
3144
  newProcess.unref();
2528
3145
  await sleep3(2000);
2529
- const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || "3000");
2530
- const actualPid = await findPidByPort(resolvedPort, 1e4) ?? await findChildPid(newProcess.pid);
3146
+ let actualPid = explicitPort && explicitPort > 0 ? await findPidByPort(explicitPort, 1e4) ?? await findChildPid(newProcess.pid) : await findChildPid(newProcess.pid);
3147
+ if (!await isProcessRunning(actualPid)) {
3148
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3149
+ if (detachedPid)
3150
+ actualPid = detachedPid;
3151
+ }
2531
3152
  let actualPort = null;
2532
3153
  for (let attempt = 0;attempt < 10; attempt++) {
2533
- const ports = await getProcessPorts(actualPid);
2534
- if (ports.length > 0) {
2535
- actualPort = ports[0];
3154
+ const resolved = await resolvePidWithPorts(actualPid);
3155
+ actualPid = resolved.pid;
3156
+ if (resolved.ports.length > 0) {
3157
+ actualPort = resolved.ports[0];
2536
3158
  break;
2537
3159
  }
3160
+ const detachedPid = await findDetachedProcessByArg("--_serve");
3161
+ if (detachedPid && detachedPid !== actualPid) {
3162
+ actualPid = detachedPid;
3163
+ }
2538
3164
  await sleep3(1000);
2539
3165
  }
2540
3166
  await retryDatabaseOperation(() => insertProcess({
@@ -2567,75 +3193,8 @@ async function run2() {
2567
3193
  announce(msg, "BGR Dashboard");
2568
3194
  return;
2569
3195
  }
2570
- if (values.guard) {
2571
- const guardName = "bgr-guard";
2572
- const homePath3 = getHomeDir();
2573
- const bgrDir2 = join4(homePath3, ".bgr");
2574
- const existing = getProcess(guardName);
2575
- if (existing && await isProcessRunning(existing.pid)) {
2576
- announce(`Guard is already running (PID ${existing.pid})
2577
-
2578
- Use ${chalk7.yellow(`bgrun --stop ${guardName}`)} to stop it
2579
- Use ${chalk7.yellow(`bgrun --guard --force`)} to restart`, "BGR Guard");
2580
- return;
2581
- }
2582
- if (existing) {
2583
- if (await isProcessRunning(existing.pid)) {
2584
- await terminateProcess(existing.pid);
2585
- }
2586
- await retryDatabaseOperation(() => removeProcessByName(guardName));
2587
- }
2588
- const { resolve } = __require("path");
2589
- const scriptPath = resolve(process.argv[1]);
2590
- const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
2591
- const command = `bgrun --_guard-loop`;
2592
- const stdoutPath = join4(bgrDir2, `${guardName}-out.txt`);
2593
- const stderrPath = join4(bgrDir2, `${guardName}-err.txt`);
2594
- await Bun.write(stdoutPath, "");
2595
- await Bun.write(stderrPath, "");
2596
- const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
2597
- env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
2598
- cwd: bgrDir2,
2599
- stdout: "ignore",
2600
- stderr: "ignore",
2601
- detached: true
2602
- });
2603
- newProcess.unref();
2604
- await sleep3(1000);
2605
- let actualPid = await findChildPid(newProcess.pid);
2606
- if (!await isProcessRunning(actualPid)) {
2607
- const { psExec: ps } = await Promise.resolve().then(() => (init_platform(), exports_platform));
2608
- 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);
2609
- const foundPid = parseInt(result.trim());
2610
- if (!isNaN(foundPid) && foundPid > 0)
2611
- actualPid = foundPid;
2612
- }
2613
- await retryDatabaseOperation(() => insertProcess({
2614
- pid: actualPid,
2615
- workdir: bgrDir2,
2616
- command,
2617
- name: guardName,
2618
- env: "BGR_KEEP_ALIVE=false",
2619
- configPath: "",
2620
- stdout_path: stdoutPath,
2621
- stderr_path: stderrPath
2622
- }));
2623
- const msg = dedent`
2624
- ${chalk7.bold("\uD83D\uDEE1\uFE0F BGR Standalone Guard launched")}
2625
- ${chalk7.gray("\u2500".repeat(40))}
2626
-
2627
- Monitors: All processes with BGR_KEEP_ALIVE=true
2628
- Also watches: bgr-dashboard (auto-restart if it dies)
2629
- Check interval: 30 seconds
2630
- Backoff: Exponential after 5 rapid crashes
2631
-
2632
- ${chalk7.gray("\u2500".repeat(40))}
2633
- Process: ${chalk7.white(guardName)} | PID: ${chalk7.white(String(actualPid))}
2634
-
2635
- ${chalk7.yellow(`bgrun ${guardName} --logs`)} View guard logs
2636
- ${chalk7.yellow(`bgrun --stop ${guardName}`)} Stop the guard
2637
- `;
2638
- announce(msg, "BGR Guard");
3196
+ if (values.guard || values["guard-off"]) {
3197
+ await handleGuardToggle(positionals[0], Boolean(values.guard));
2639
3198
  return;
2640
3199
  }
2641
3200
  if (values.version) {
@@ -2797,7 +3356,7 @@ async function run2() {
2797
3356
  name,
2798
3357
  command: values.command,
2799
3358
  directory: values.directory,
2800
- configPath: values.config,
3359
+ configPath: values["no-config"] ? "" : values.config,
2801
3360
  force: values.force,
2802
3361
  fetch: values.fetch,
2803
3362
  remoteName: "",