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/api.js CHANGED
@@ -17,48 +17,59 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = import.meta.require;
18
18
 
19
19
  // src/platform.ts
20
- var exports_platform = {};
21
- __export(exports_platform, {
22
- waitForPortFree: () => waitForPortFree,
23
- terminateProcess: () => terminateProcess,
24
- reconcileProcessPids: () => reconcileProcessPids,
25
- readFileTail: () => readFileTail,
26
- psExec: () => psExec,
27
- parseUnixListeningPorts: () => parseUnixListeningPorts,
28
- killProcessOnPort: () => killProcessOnPort,
29
- isWindows: () => isWindows,
30
- isProcessRunning: () => isProcessRunning,
31
- isPortFree: () => isPortFree,
32
- getShellCommand: () => getShellCommand,
33
- getProcessPorts: () => getProcessPorts,
34
- getProcessMemory: () => getProcessMemory,
35
- getProcessBatchResources: () => getProcessBatchResources,
36
- getPortInfo: () => getPortInfo,
37
- getHomeDir: () => getHomeDir,
38
- findPidByPort: () => findPidByPort,
39
- findChildPid: () => findChildPid,
40
- ensureDir: () => ensureDir,
41
- copyFile: () => copyFile
42
- });
43
20
  import * as fs from "fs";
44
21
  import * as os from "os";
45
22
  import { join } from "path";
46
23
  var {$ } = globalThis.Bun;
47
24
  import { createMeasure } from "measure-fn";
48
- function psExec(command, _timeoutMs = 3000) {
49
- const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
25
+ async function psExec(command, timeoutMs = 3000) {
26
+ const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.ps1`);
50
27
  try {
51
- fs.writeFileSync(tmpFile, command);
52
- const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
28
+ await Bun.write(tmpFile, command);
29
+ const proc = Bun.spawn([
30
+ "powershell",
31
+ "-NoProfile",
32
+ "-ExecutionPolicy",
33
+ "Bypass",
34
+ "-File",
35
+ tmpFile
36
+ ], {
37
+ stdout: "pipe",
38
+ stderr: "pipe"
39
+ });
40
+ const timeoutPromise = new Promise((_, reject) => {
41
+ setTimeout(() => reject(new Error("PowerShell command timed out")), timeoutMs);
42
+ });
43
+ const resultPromise = new Promise(async (resolve, reject) => {
44
+ try {
45
+ const stdoutPromise = proc.stdout ? new Response(proc.stdout).text() : Promise.resolve("");
46
+ const stderrPromise = proc.stderr ? new Response(proc.stderr).text() : Promise.resolve("");
47
+ const exitCode = await proc.exited;
48
+ const stdout = await stdoutPromise;
49
+ const stderr = await stderrPromise;
50
+ if (exitCode === 0) {
51
+ resolve(stdout);
52
+ } else {
53
+ resolve(stderr || "");
54
+ }
55
+ } catch (error) {
56
+ reject(error);
57
+ }
58
+ });
53
59
  try {
54
- fs.unlinkSync(tmpFile);
55
- } catch {}
56
- return result.stdout?.toString() || "";
57
- } catch {
60
+ const result = await Promise.race([resultPromise, timeoutPromise]);
61
+ return result.trim();
62
+ } catch (error) {
63
+ return "";
64
+ } finally {
65
+ try {
66
+ await Bun.sleep(100);
67
+ } catch {}
68
+ }
69
+ } finally {
58
70
  try {
59
- fs.unlinkSync(tmpFile);
71
+ fs.rmSync(tmpFile, { force: true });
60
72
  } catch {}
61
- return "";
62
73
  }
63
74
  }
64
75
  function isWindows() {
@@ -80,7 +91,7 @@ async function isProcessRunning(pid, command) {
80
91
  process.kill(pid, 0);
81
92
  return true;
82
93
  } catch {
83
- const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
94
+ const output = await psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`);
84
95
  return output === String(pid);
85
96
  }
86
97
  } else {
@@ -173,35 +184,6 @@ async function isPortFree(port) {
173
184
  return true;
174
185
  }
175
186
  }
176
- async function getPortInfo(port) {
177
- try {
178
- if (isWindows()) {
179
- const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
180
- for (const line of result.split(`
181
- `)) {
182
- const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
183
- if (match) {
184
- const pid = parseInt(match[2]);
185
- if (pid > 0 && await isProcessRunning(pid)) {
186
- const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
187
- return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
188
- }
189
- }
190
- }
191
- return { inUse: false };
192
- } else {
193
- const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
194
- const lines = result.trim().split(`
195
- `).filter((l) => l.trim());
196
- if (lines.length > 1) {
197
- return { inUse: true };
198
- }
199
- return { inUse: false };
200
- }
201
- } catch {
202
- return { inUse: false };
203
- }
204
- }
205
187
  async function waitForPortFree(port, timeoutMs = 5000) {
206
188
  const startTime = Date.now();
207
189
  const pollInterval = 300;
@@ -409,9 +391,6 @@ async function readFileTail(filePath, lines) {
409
391
  }
410
392
  }) ?? "";
411
393
  }
412
- function copyFile(src, dest) {
413
- fs.copyFileSync(src, dest);
414
- }
415
394
  async function getProcessMemory(pid) {
416
395
  const map = await getProcessBatchResources([pid]);
417
396
  return map.get(pid)?.memory || 0;
@@ -424,7 +403,7 @@ async function getProcessBatchResources(pids) {
424
403
  const pidSet = new Set(pids);
425
404
  try {
426
405
  if (isWindows()) {
427
- const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
406
+ const output = await psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
428
407
  for (const line of output.split(`
429
408
  `)) {
430
409
  const sepIdx = line.indexOf("|");
@@ -504,6 +483,21 @@ async function getProcessPorts(pid) {
504
483
  return [];
505
484
  }
506
485
  }
486
+ async function resolvePidWithPorts(pid) {
487
+ const ports = await getProcessPorts(pid);
488
+ if (ports.length > 0 || !isWindows() || pid <= 0) {
489
+ return { pid, ports };
490
+ }
491
+ const childPid = await findChildPid(pid);
492
+ if (childPid === pid || childPid <= 0) {
493
+ return { pid, ports };
494
+ }
495
+ const childPorts = await getProcessPorts(childPid);
496
+ if (childPorts.length > 0) {
497
+ return { pid: childPid, ports: childPorts };
498
+ }
499
+ return { pid, ports };
500
+ }
507
501
  var plat;
508
502
  var init_platform = __esm(() => {
509
503
  plat = createMeasure("platform");
@@ -580,7 +574,7 @@ function removeProcessByName(name) {
580
574
  function updateProcessPid(name, newPid) {
581
575
  const proc = db.process.select().where({ name }).limit(1).get();
582
576
  if (proc) {
583
- db.process.update(proc.id, { pid: newPid });
577
+ proc.update({ pid: newPid });
584
578
  }
585
579
  }
586
580
  function removeAllProcesses() {
@@ -592,7 +586,7 @@ function removeAllProcesses() {
592
586
  function updateProcessEnv(name, envJson) {
593
587
  const proc = db.process.select().where({ name }).limit(1).get();
594
588
  if (proc) {
595
- db.process.update(proc.id, { env: envJson });
589
+ proc.update({ env: envJson });
596
590
  }
597
591
  }
598
592
  function getAllTemplates() {
@@ -830,6 +824,8 @@ var init_db = __esm(() => {
830
824
 
831
825
  // src/utils.ts
832
826
  import * as fs2 from "fs";
827
+ import * as os2 from "os";
828
+ import { join as join3 } from "path";
833
829
  function parseEnvString(envString) {
834
830
  const env = {};
835
831
  envString.split(",").forEach((pair) => {
@@ -845,9 +841,92 @@ function calculateRuntime(startTime) {
845
841
  const diffInMinutes = Math.floor((now - start) / (1000 * 60));
846
842
  return `${diffInMinutes} minutes`;
847
843
  }
844
+ function parseCommandEnv(command) {
845
+ const env = {};
846
+ const trimmed = command.trim();
847
+ const windowsSegments = trimmed.split(/&&/).map((segment) => segment.trim());
848
+ for (const segment of windowsSegments) {
849
+ const match = segment.match(/^set\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/i);
850
+ if (!match)
851
+ break;
852
+ env[match[1]] = match[2].trim();
853
+ }
854
+ const unixPrefixRegex = /^(?:([A-Za-z_][A-Za-z0-9_]*)=([^\s]+)\s+)+/;
855
+ const unixPrefix = trimmed.match(unixPrefixRegex);
856
+ if (unixPrefix) {
857
+ const pairs = unixPrefix[0].trim().split(/\s+/);
858
+ for (const pair of pairs) {
859
+ const eqIdx = pair.indexOf("=");
860
+ if (eqIdx <= 0)
861
+ continue;
862
+ const key = pair.slice(0, eqIdx);
863
+ const value = pair.slice(eqIdx + 1);
864
+ if (key)
865
+ env[key] = value;
866
+ }
867
+ }
868
+ return env;
869
+ }
870
+ function getDeclaredPort(processEnv, command) {
871
+ const mergedEnv = { ...command ? parseCommandEnv(command) : {}, ...processEnv };
872
+ const raw = mergedEnv.PORT || mergedEnv.BUN_PORT || "";
873
+ const parsed = parseInt(raw, 10);
874
+ return !isNaN(parsed) && parsed > 0 ? parsed : null;
875
+ }
876
+ function buildManagedProcessEnv(parentEnv, processEnv = {}) {
877
+ const sanitizedParentEnv = {};
878
+ for (const [key, value] of Object.entries(parentEnv)) {
879
+ if (value === undefined)
880
+ continue;
881
+ if (INTERNAL_MANAGED_ENV_KEYS.includes(key))
882
+ continue;
883
+ sanitizedParentEnv[key] = value;
884
+ }
885
+ return { ...sanitizedParentEnv, ...processEnv };
886
+ }
887
+ function stringifyEnvString(env) {
888
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`).join(",");
889
+ }
890
+ function getWatcherProcessName(targetName) {
891
+ return `${WATCHER_PREFIX}${encodeURIComponent(targetName)}`;
892
+ }
893
+ function getWatchedProcessName(watcherName) {
894
+ if (!watcherName.startsWith(WATCHER_PREFIX))
895
+ return null;
896
+ try {
897
+ return decodeURIComponent(watcherName.slice(WATCHER_PREFIX.length));
898
+ } catch {
899
+ return null;
900
+ }
901
+ }
902
+ function isWatcherProcessName(name) {
903
+ return getWatchedProcessName(name) !== null;
904
+ }
905
+ function isInternalProcessName(name) {
906
+ return name === "bgr-dashboard" || name === "bgr-guard" || isWatcherProcessName(name);
907
+ }
908
+ function getOperationLockPath(name) {
909
+ return join3(os2.homedir(), ".bgr", `${name}.operation.lock`);
910
+ }
911
+ function acquireProcessOperationLock(name) {
912
+ const lockPath = getOperationLockPath(name);
913
+ fs2.mkdirSync(join3(os2.homedir(), ".bgr"), { recursive: true });
914
+ fs2.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, time: Date.now() }));
915
+ let released = false;
916
+ return () => {
917
+ if (released)
918
+ return;
919
+ released = true;
920
+ try {
921
+ fs2.unlinkSync(lockPath);
922
+ } catch {}
923
+ };
924
+ }
925
+ function isProcessOperationLocked(name) {
926
+ return fs2.existsSync(getOperationLockPath(name));
927
+ }
848
928
  async function getVersion() {
849
929
  try {
850
- const { join: join3 } = await import("path");
851
930
  const pkgPath = join3(import.meta.dir, "../package.json");
852
931
  const pkg = await Bun.file(pkgPath).json();
853
932
  return pkg.version || "0.0.0";
@@ -903,8 +982,10 @@ function tailFile(path, prefix, colorFn, lines) {
903
982
  } catch {}
904
983
  };
905
984
  }
985
+ var INTERNAL_MANAGED_ENV_KEYS, WATCHER_PREFIX = "bgr-watch-";
906
986
  var init_utils = __esm(() => {
907
987
  init_platform();
988
+ INTERNAL_MANAGED_ENV_KEYS = ["BUN_PORT", "BGR_STDOUT", "BGR_STDERR"];
908
989
  });
909
990
 
910
991
  // src/deps.ts
@@ -996,6 +1077,14 @@ var init_deps = __esm(() => {
996
1077
  init_utils();
997
1078
  });
998
1079
 
1080
+ // src/api.ts
1081
+ init_db();
1082
+ init_platform();
1083
+
1084
+ // src/commands/run.ts
1085
+ init_db();
1086
+ init_platform();
1087
+
999
1088
  // src/logger.ts
1000
1089
  import boxen from "boxen";
1001
1090
  import chalk from "chalk";
@@ -1009,6 +1098,13 @@ function announce(message, title) {
1009
1098
  borderStyle: "round"
1010
1099
  }));
1011
1100
  }
1101
+
1102
+ class BgrunError extends Error {
1103
+ constructor(message) {
1104
+ super(message);
1105
+ this.name = "BgrunError";
1106
+ }
1107
+ }
1012
1108
  function error(message) {
1013
1109
  const text = message instanceof Error ? message.stack || message.message : String(message);
1014
1110
  console.error(boxen(chalk.red(text), {
@@ -1021,15 +1117,9 @@ function error(message) {
1021
1117
  }));
1022
1118
  throw new BgrunError(text);
1023
1119
  }
1024
- var BgrunError;
1025
- var init_logger = __esm(() => {
1026
- BgrunError = class BgrunError extends Error {
1027
- constructor(message) {
1028
- super(message);
1029
- this.name = "BgrunError";
1030
- }
1031
- };
1032
- });
1120
+
1121
+ // src/commands/run.ts
1122
+ init_utils();
1033
1123
 
1034
1124
  // src/config.ts
1035
1125
  function formatEnvKey(key) {
@@ -1061,190 +1151,738 @@ async function parseConfigFile(configPath) {
1061
1151
  const parsedConfig = await import(importPath).then((m) => m.default);
1062
1152
  return flattenConfig(parsedConfig);
1063
1153
  }
1154
+ async function loadConfigEnv(directory, configPath = ".config.toml") {
1155
+ const { join: join4 } = await import("path");
1156
+ const fullConfigPath = join4(directory, configPath);
1157
+ if (!await Bun.file(fullConfigPath).exists()) {
1158
+ return {
1159
+ configEnv: {},
1160
+ fullConfigPath,
1161
+ exists: false
1162
+ };
1163
+ }
1164
+ return {
1165
+ configEnv: await parseConfigFile(fullConfigPath),
1166
+ fullConfigPath,
1167
+ exists: true
1168
+ };
1169
+ }
1064
1170
 
1065
1171
  // src/commands/run.ts
1066
1172
  var {$: $2 } = globalThis.Bun;
1067
1173
  var {sleep: sleep2 } = globalThis.Bun;
1068
- import { join as join3 } from "path";
1174
+ import { join as join5 } from "path";
1069
1175
  import { createMeasure as createMeasure2 } from "measure-fn";
1070
- async function handleRun(options) {
1071
- const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
1072
- const existingProcess = name ? getProcess(name) : null;
1073
- if (name && existingProcess) {
1074
- const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1075
- const unmet = await getUnmetDeps2(name);
1076
- if (unmet.length > 0) {
1077
- await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1078
- for (const depName of unmet) {
1079
- const depProc = getProcess(depName);
1080
- if (depProc) {
1081
- announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1082
- await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
1083
- }
1084
- }
1085
- });
1176
+
1177
+ // src/watcher.ts
1178
+ init_db();
1179
+ init_platform();
1180
+ import { join as join4 } from "path";
1181
+
1182
+ // src/cli-helpers.ts
1183
+ init_db();
1184
+ var MONTH_NAMES = [
1185
+ "january",
1186
+ "february",
1187
+ "march",
1188
+ "april",
1189
+ "may",
1190
+ "june",
1191
+ "july",
1192
+ "august",
1193
+ "september",
1194
+ "october",
1195
+ "november",
1196
+ "december"
1197
+ ];
1198
+ var DAY_NAMES = [
1199
+ "",
1200
+ "first",
1201
+ "second",
1202
+ "third",
1203
+ "fourth",
1204
+ "fifth",
1205
+ "sixth",
1206
+ "seventh",
1207
+ "eighth",
1208
+ "ninth",
1209
+ "tenth",
1210
+ "eleventh",
1211
+ "twelfth",
1212
+ "thirteenth",
1213
+ "fourteenth",
1214
+ "fifteenth",
1215
+ "sixteenth",
1216
+ "seventeenth",
1217
+ "eighteenth",
1218
+ "nineteenth",
1219
+ "twentieth",
1220
+ "twenty-first",
1221
+ "twenty-second",
1222
+ "twenty-third",
1223
+ "twenty-fourth",
1224
+ "twenty-fifth",
1225
+ "twenty-sixth",
1226
+ "twenty-seventh",
1227
+ "twenty-eighth",
1228
+ "twenty-ninth",
1229
+ "thirtieth",
1230
+ "thirty-first"
1231
+ ];
1232
+ function pad2(value) {
1233
+ return String(value).padStart(2, "0");
1234
+ }
1235
+ function buildDateProcessName(now = new Date) {
1236
+ const month = MONTH_NAMES[now.getMonth()] || "process";
1237
+ const day = DAY_NAMES[now.getDate()] || "day";
1238
+ return `${month}-${day}`;
1239
+ }
1240
+ function generateAutoProcessName(now = new Date) {
1241
+ const baseName = buildDateProcessName(now);
1242
+ if (!getProcess(baseName)) {
1243
+ return baseName;
1244
+ }
1245
+ const timeSuffix = `${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}`;
1246
+ const timeName = `${baseName}-${timeSuffix}`;
1247
+ if (!getProcess(timeName)) {
1248
+ return timeName;
1249
+ }
1250
+ for (let i = 1;i < 1000; i++) {
1251
+ const candidate = `${timeName}-${i}`;
1252
+ if (!getProcess(candidate)) {
1253
+ return candidate;
1086
1254
  }
1087
1255
  }
1088
- if (existingProcess) {
1089
- const finalDirectory2 = directory || existingProcess.workdir;
1090
- validateDirectory(finalDirectory2);
1091
- $2.cwd(finalDirectory2);
1092
- if (fetch) {
1093
- if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1094
- error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1256
+ return `${timeName}-${Date.now()}`;
1257
+ }
1258
+ function shellQuoteArg(arg) {
1259
+ if (process.platform === "win32") {
1260
+ if (/^[A-Za-z0-9_./:\\-]+$/.test(arg))
1261
+ return arg;
1262
+ return `"${arg.replace(/"/g, "\\\"")}"`;
1263
+ }
1264
+ if (/^[A-Za-z0-9_./:-]+$/.test(arg))
1265
+ return arg;
1266
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
1267
+ }
1268
+ function joinCommandArgs(args) {
1269
+ return args.map(shellQuoteArg).join(" ");
1270
+ }
1271
+
1272
+ // src/watcher.ts
1273
+ init_utils();
1274
+ var DEFAULT_INTERVAL_MS = 5000;
1275
+ var CRASH_THRESHOLD = 5;
1276
+ var MAX_BACKOFF_MS = 5 * 60000;
1277
+ var STABILITY_WINDOW_MS = 120000;
1278
+ function getWatcherLogPaths(watcherName) {
1279
+ const homePath2 = getHomeDir();
1280
+ return {
1281
+ stdoutPath: join4(homePath2, ".bgr", `${watcherName}-out.txt`),
1282
+ stderrPath: join4(homePath2, ".bgr", `${watcherName}-err.txt`)
1283
+ };
1284
+ }
1285
+ async function findDetachedWatcherPid(targetName) {
1286
+ if (process.platform !== "win32")
1287
+ return null;
1288
+ const escaped = targetName.replace(/'/g, "''");
1289
+ 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);
1290
+ const pid = parseInt(result.trim(), 10);
1291
+ return !isNaN(pid) && pid > 0 ? pid : null;
1292
+ }
1293
+ function getInternalWatcherCommand(targetName) {
1294
+ const quotedTarget = shellQuoteArg(targetName);
1295
+ return {
1296
+ storedCommand: `bunx bgrun --_watch-process ${quotedTarget}`,
1297
+ spawnCommand: `bunx bgrun --_watch-process ${quotedTarget}`
1298
+ };
1299
+ }
1300
+ async function ensureProcessWatcher(targetName) {
1301
+ if (!targetName || isInternalProcessName(targetName))
1302
+ return;
1303
+ const proc = getProcess(targetName);
1304
+ if (!proc)
1305
+ return;
1306
+ const env = parseEnvString(proc.env || "");
1307
+ if (env.BGR_KEEP_ALIVE !== "true") {
1308
+ await stopProcessWatcher(targetName);
1309
+ return;
1310
+ }
1311
+ const watcherName = getWatcherProcessName(targetName);
1312
+ const existingWatcher = getProcess(watcherName);
1313
+ if (existingWatcher && await isProcessRunning(existingWatcher.pid, existingWatcher.command)) {
1314
+ return;
1315
+ }
1316
+ if (existingWatcher) {
1317
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1318
+ }
1319
+ const { stdoutPath, stderrPath } = getWatcherLogPaths(watcherName);
1320
+ await Bun.write(stdoutPath, "");
1321
+ await Bun.write(stderrPath, "");
1322
+ const { storedCommand, spawnCommand } = getInternalWatcherCommand(targetName);
1323
+ const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
1324
+ env: {
1325
+ ...Bun.env,
1326
+ BGR_STDOUT: stdoutPath,
1327
+ BGR_STDERR: stderrPath
1328
+ },
1329
+ cwd: getHomeDir(),
1330
+ stdout: "ignore",
1331
+ stderr: "ignore",
1332
+ detached: true
1333
+ });
1334
+ newProcess.unref();
1335
+ await Bun.sleep(1000);
1336
+ let actualPid = await findChildPid(newProcess.pid);
1337
+ if (!await isProcessRunning(actualPid)) {
1338
+ const detachedPid = await findDetachedWatcherPid(targetName);
1339
+ if (detachedPid)
1340
+ actualPid = detachedPid;
1341
+ }
1342
+ await retryDatabaseOperation(() => insertProcess({
1343
+ pid: actualPid,
1344
+ workdir: getHomeDir(),
1345
+ command: storedCommand,
1346
+ name: watcherName,
1347
+ env: stringifyEnvString({ BGR_KEEP_ALIVE: "false", BGR_WATCH_TARGET: targetName }),
1348
+ configPath: "",
1349
+ stdout_path: stdoutPath,
1350
+ stderr_path: stderrPath
1351
+ }));
1352
+ }
1353
+ async function stopProcessWatcher(targetName) {
1354
+ const watcherName = getWatcherProcessName(targetName);
1355
+ const watcherProc = getProcess(watcherName);
1356
+ if (!watcherProc)
1357
+ return;
1358
+ if (await isProcessRunning(watcherProc.pid, watcherProc.command)) {
1359
+ await terminateProcess(watcherProc.pid, true);
1360
+ }
1361
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1362
+ }
1363
+ async function syncProcessWatcher(targetName, env) {
1364
+ if (!targetName || isInternalProcessName(targetName))
1365
+ return;
1366
+ if (env.BGR_KEEP_ALIVE === "true") {
1367
+ await ensureProcessWatcher(targetName);
1368
+ } else {
1369
+ await stopProcessWatcher(targetName);
1370
+ }
1371
+ }
1372
+ function getBackoffMs(restartCount) {
1373
+ if (restartCount <= CRASH_THRESHOLD)
1374
+ return 0;
1375
+ const exponent = restartCount - CRASH_THRESHOLD;
1376
+ return Math.min(30000 * Math.pow(2, exponent - 1), MAX_BACKOFF_MS);
1377
+ }
1378
+ async function cleanupWatcher(targetName) {
1379
+ const watcherName = getWatcherProcessName(targetName);
1380
+ await retryDatabaseOperation(() => removeProcessByName(watcherName));
1381
+ }
1382
+ async function startProcessWatcher(targetName, intervalMs = DEFAULT_INTERVAL_MS) {
1383
+ const watcherName = getWatcherProcessName(targetName);
1384
+ const releaseWatcherLock = acquireProcessOperationLock(watcherName);
1385
+ let restartCount = 0;
1386
+ let nextRestartAt = 0;
1387
+ let lastSeenAliveAt = 0;
1388
+ try {
1389
+ console.log(`[watcher] Watching "${targetName}" every ${Math.round(intervalMs / 1000)}s`);
1390
+ while (true) {
1391
+ const proc = getProcess(targetName);
1392
+ if (!proc) {
1393
+ console.log(`[watcher] Target "${targetName}" removed; exiting watcher`);
1394
+ break;
1395
+ }
1396
+ const env = parseEnvString(proc.env || "");
1397
+ if (env.BGR_KEEP_ALIVE !== "true") {
1398
+ console.log(`[watcher] Guard disabled for "${targetName}"; exiting watcher`);
1399
+ break;
1400
+ }
1401
+ if (proc.pid <= 0 || isProcessOperationLocked(targetName)) {
1402
+ await Bun.sleep(intervalMs);
1403
+ continue;
1095
1404
  }
1096
- await run.measure(`Git fetch "${name}"`, async () => {
1405
+ const alive = await isProcessRunning(proc.pid, proc.command);
1406
+ if (!alive) {
1407
+ const now = Date.now();
1408
+ if (now < nextRestartAt) {
1409
+ await Bun.sleep(intervalMs);
1410
+ continue;
1411
+ }
1097
1412
  try {
1098
- await $2`git fetch origin`;
1099
- const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1100
- const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1101
- if (localHash !== remoteHash) {
1102
- await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1103
- announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1104
- }
1413
+ console.log(`[watcher] Restarting "${targetName}" after detected crash`);
1414
+ await handleRun({
1415
+ action: "run",
1416
+ name: targetName,
1417
+ force: true,
1418
+ remoteName: ""
1419
+ });
1420
+ restartCount++;
1421
+ const backoffMs = getBackoffMs(restartCount);
1422
+ nextRestartAt = backoffMs > 0 ? now + backoffMs : 0;
1423
+ lastSeenAliveAt = 0;
1424
+ addHistoryEntry(targetName, "guard_restart", proc.pid, { by: watcherName, count: restartCount, backoffMs });
1105
1425
  } catch (err) {
1106
- error(`Failed to pull latest changes: ${err}`);
1426
+ addHistoryEntry(targetName, "guard_restart_failed", proc.pid, { by: watcherName, error: err?.message || String(err) });
1427
+ console.error(`[watcher] Failed to restart "${targetName}": ${err.message}`);
1107
1428
  }
1108
- });
1109
- }
1110
- const isRunning = await isProcessRunning(existingProcess.pid);
1111
- if (isRunning && !force) {
1112
- error(`Process '${name}' is currently running. Use --force to restart.`);
1113
- }
1114
- let detectedPorts = [];
1115
- if (isRunning) {
1116
- detectedPorts = await getProcessPorts(existingProcess.pid);
1429
+ } else if (restartCount > 0) {
1430
+ const now = Date.now();
1431
+ if (!lastSeenAliveAt) {
1432
+ lastSeenAliveAt = now;
1433
+ } else if (now - lastSeenAliveAt >= STABILITY_WINDOW_MS) {
1434
+ restartCount = 0;
1435
+ nextRestartAt = 0;
1436
+ lastSeenAliveAt = 0;
1437
+ }
1438
+ }
1439
+ await Bun.sleep(intervalMs);
1117
1440
  }
1118
- if (isRunning) {
1119
- await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
1120
- await terminateProcess(existingProcess.pid);
1121
- announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1122
- });
1441
+ } finally {
1442
+ releaseWatcherLock();
1443
+ await cleanupWatcher(targetName);
1444
+ }
1445
+ }
1446
+ function getGuardRestartCounts() {
1447
+ const counts = new Map;
1448
+ const entries = db.history.select().where({ event: "guard_restart" }).all();
1449
+ for (const entry of entries) {
1450
+ counts.set(entry.process_name, (counts.get(entry.process_name) || 0) + 1);
1451
+ }
1452
+ return counts;
1453
+ }
1454
+ function getRecentGuardEvents(limit = 100) {
1455
+ 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);
1456
+ return rows.map((row) => ({
1457
+ time: new Date(row.timestamp).getTime(),
1458
+ name: row.process_name,
1459
+ action: "restart",
1460
+ success: row.event === "guard_restart"
1461
+ }));
1462
+ }
1463
+
1464
+ // src/commands/run.ts
1465
+ var homePath2 = getHomeDir();
1466
+ var run = createMeasure2("run");
1467
+ var INTERNAL_BUNX_PREFIX = "bunx bgrun";
1468
+ function resolveInternalBgrunCommand(command) {
1469
+ const trimmed = command.trim();
1470
+ if (!trimmed.startsWith("bgrun --_") && !trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
1471
+ return command;
1472
+ }
1473
+ if (trimmed.startsWith(`${INTERNAL_BUNX_PREFIX} --_`)) {
1474
+ return trimmed;
1475
+ }
1476
+ return `${INTERNAL_BUNX_PREFIX}${trimmed.slice("bgrun".length)}`;
1477
+ }
1478
+ async function handleRun(options) {
1479
+ const {
1480
+ command,
1481
+ directory,
1482
+ env,
1483
+ name,
1484
+ configPath,
1485
+ force,
1486
+ fetch,
1487
+ stdout,
1488
+ stderr
1489
+ } = options;
1490
+ const releaseOperationLock = name ? acquireProcessOperationLock(name) : () => {};
1491
+ try {
1492
+ const existingProcess = name ? getProcess(name) : null;
1493
+ if (name && existingProcess) {
1494
+ const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
1495
+ const unmet = await getUnmetDeps2(name);
1496
+ if (unmet.length > 0) {
1497
+ await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
1498
+ for (const depName of unmet) {
1499
+ const depProc = getProcess(depName);
1500
+ if (depProc) {
1501
+ announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
1502
+ await handleRun({
1503
+ action: "run",
1504
+ name: depName,
1505
+ force: true,
1506
+ remoteName: ""
1507
+ });
1508
+ }
1509
+ }
1510
+ });
1511
+ }
1123
1512
  }
1124
- if (detectedPorts.length > 0) {
1125
- await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1126
- for (const port of detectedPorts) {
1127
- await killProcessOnPort(port);
1513
+ if (existingProcess) {
1514
+ const finalDirectory2 = directory || existingProcess.workdir;
1515
+ validateDirectory(finalDirectory2);
1516
+ $2.cwd(finalDirectory2);
1517
+ if (fetch) {
1518
+ if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
1519
+ error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
1128
1520
  }
1129
- for (const port of detectedPorts) {
1130
- const freed = await waitForPortFree(port, 5000);
1131
- if (!freed) {
1132
- await killProcessOnPort(port);
1133
- await waitForPortFree(port, 3000);
1521
+ await run.measure(`Git fetch "${name}"`, async () => {
1522
+ try {
1523
+ await $2`git fetch origin`;
1524
+ const localHash = (await $2`git rev-parse HEAD`.text()).trim();
1525
+ const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
1526
+ if (localHash !== remoteHash) {
1527
+ await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
1528
+ announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
1529
+ }
1530
+ } catch (err) {
1531
+ error(`Failed to pull latest changes: ${err}`);
1134
1532
  }
1533
+ });
1534
+ }
1535
+ const isRunning = await isProcessRunning(existingProcess.pid);
1536
+ if (isRunning && !force) {
1537
+ error(`Process '${name}' is currently running. Use --force to restart.`);
1538
+ }
1539
+ let actualPid2 = existingProcess.pid;
1540
+ if (!isRunning && !force) {
1541
+ const reconciled = await reconcileProcessPids([
1542
+ {
1543
+ name,
1544
+ pid: existingProcess.pid,
1545
+ command: existingProcess.command,
1546
+ workdir: existingProcess.workdir
1547
+ }
1548
+ ], new Set([existingProcess.pid]));
1549
+ const newPid = reconciled.get(name);
1550
+ if (newPid) {
1551
+ console.log(`[run] Reconciled dead PID ${existingProcess.pid} to live PID ${newPid}`);
1552
+ actualPid2 = newPid;
1553
+ updateProcessPid(name, newPid);
1135
1554
  }
1136
- });
1137
- }
1138
- const cmdToMatch = existingProcess.command;
1139
- if (cmdToMatch) {
1140
- await run.measure("Zombie sweep", async () => {
1141
- try {
1142
- const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1143
- const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
1144
- if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1145
- return;
1555
+ }
1556
+ let detectedPorts = [];
1557
+ const actuallyRunning = await isProcessRunning(actualPid2);
1558
+ if (actuallyRunning) {
1559
+ detectedPorts = await getProcessPorts(actualPid2);
1560
+ }
1561
+ if (actuallyRunning) {
1562
+ await run.measure(`Terminate "${name}" (PID ${actualPid2})`, async () => {
1563
+ await terminateProcess(actualPid2);
1564
+ announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
1565
+ });
1566
+ }
1567
+ if (detectedPorts.length > 0) {
1568
+ await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
1569
+ for (const port of detectedPorts) {
1570
+ await killProcessOnPort(port);
1146
1571
  }
1147
- const currentPid = process.pid;
1148
- 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);
1149
- const zombiePids = result.split(`
1150
- `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1151
- for (const zPid of zombiePids) {
1152
- await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1572
+ for (const port of detectedPorts) {
1573
+ const freed = await waitForPortFree(port, 5000);
1574
+ if (!freed) {
1575
+ await killProcessOnPort(port);
1576
+ await waitForPortFree(port, 3000);
1577
+ }
1153
1578
  }
1154
- if (zombiePids.length > 0) {
1155
- announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1579
+ });
1580
+ }
1581
+ const existingEnv = existingProcess.env ? parseEnvString(existingProcess.env) : {};
1582
+ const declaredPort = getDeclaredPort(existingEnv, existingProcess.command);
1583
+ if (declaredPort && !detectedPorts.includes(declaredPort)) {
1584
+ await run.measure(`Declared port cleanup [${declaredPort}]`, async () => {
1585
+ const portFree = await isPortFree(declaredPort);
1586
+ if (!portFree) {
1587
+ console.log(`[run] Declared port ${declaredPort} is busy (orphaned process), cleaning up...`);
1588
+ await killProcessOnPort(declaredPort);
1589
+ const freed = await waitForPortFree(declaredPort, 5000);
1590
+ if (!freed) {
1591
+ console.warn(`[run] Port ${declaredPort} still busy after cleanup, retrying...`);
1592
+ await killProcessOnPort(declaredPort);
1593
+ await waitForPortFree(declaredPort, 3000);
1594
+ }
1156
1595
  }
1157
- } catch {}
1158
- });
1596
+ });
1597
+ }
1598
+ const cmdToMatch = existingProcess.command;
1599
+ if (cmdToMatch) {
1600
+ await run.measure("Zombie sweep", async () => {
1601
+ try {
1602
+ const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
1603
+ const GENERIC_KEYWORDS = [
1604
+ "dev",
1605
+ "run",
1606
+ "start",
1607
+ "serve",
1608
+ "build",
1609
+ "test"
1610
+ ];
1611
+ if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
1612
+ return;
1613
+ }
1614
+ const currentPid = process.pid;
1615
+ 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);
1616
+ const zombiePids = result.split(`
1617
+ `).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
1618
+ for (const zPid of zombiePids) {
1619
+ await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
1620
+ }
1621
+ if (zombiePids.length > 0) {
1622
+ announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
1623
+ }
1624
+ } catch {}
1625
+ });
1626
+ }
1627
+ await retryDatabaseOperation(() => removeProcessByName(name));
1628
+ } else {
1629
+ if (!directory || !name || !command) {
1630
+ error("'directory', 'name', and 'command' parameters are required for new processes.");
1631
+ }
1632
+ validateDirectory(directory);
1633
+ $2.cwd(directory);
1159
1634
  }
1160
- await retryDatabaseOperation(() => removeProcessByName(name));
1161
- } else {
1162
- if (!directory || !name || !command) {
1163
- error("'directory', 'name', and 'command' parameters are required for new processes.");
1635
+ const storedCommand = command || existingProcess.command;
1636
+ const finalCommand = resolveInternalBgrunCommand(storedCommand);
1637
+ const finalDirectory = directory || existingProcess?.workdir;
1638
+ let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1639
+ let finalConfigPath;
1640
+ if (configPath !== undefined) {
1641
+ finalConfigPath = configPath;
1642
+ } else if (existingProcess) {
1643
+ finalConfigPath = existingProcess.configPath;
1644
+ } else {
1645
+ finalConfigPath = ".config.toml";
1164
1646
  }
1165
- validateDirectory(directory);
1166
- $2.cwd(directory);
1167
- }
1168
- const finalCommand = command || existingProcess.command;
1169
- const finalDirectory = directory || existingProcess?.workdir;
1170
- let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
1171
- if (!("BGR_KEEP_ALIVE" in finalEnv)) {
1172
- finalEnv.BGR_KEEP_ALIVE = "true";
1173
- }
1174
- let finalConfigPath;
1175
- if (configPath !== undefined) {
1176
- finalConfigPath = configPath;
1177
- } else if (existingProcess) {
1178
- finalConfigPath = existingProcess.configPath;
1179
- } else {
1180
- finalConfigPath = ".config.toml";
1181
- }
1182
- if (finalConfigPath) {
1183
- const fullConfigPath = join3(finalDirectory, finalConfigPath);
1184
- if (await Bun.file(fullConfigPath).exists()) {
1185
- const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1186
- try {
1187
- return await parseConfigFile(fullConfigPath);
1188
- } catch (err) {
1189
- console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1190
- return null;
1647
+ if (finalConfigPath) {
1648
+ const fullConfigPath = join5(finalDirectory, finalConfigPath);
1649
+ if (await Bun.file(fullConfigPath).exists()) {
1650
+ const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
1651
+ try {
1652
+ return await parseConfigFile(fullConfigPath);
1653
+ } catch (err) {
1654
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
1655
+ return null;
1656
+ }
1657
+ });
1658
+ if (configEnv) {
1659
+ finalEnv = { ...finalEnv, ...configEnv };
1660
+ console.log(`Loaded config from ${finalConfigPath}`);
1191
1661
  }
1192
- });
1193
- if (configEnv) {
1194
- finalEnv = { ...finalEnv, ...configEnv };
1195
- console.log(`Loaded config from ${finalConfigPath}`);
1662
+ } else {
1663
+ console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1196
1664
  }
1197
- } else {
1198
- console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
1199
1665
  }
1666
+ const stdoutPath = stdout || existingProcess?.stdout_path || join5(homePath2, ".bgr", `${name}-out.txt`);
1667
+ Bun.write(stdoutPath, "");
1668
+ const stderrPath = stderr || existingProcess?.stderr_path || join5(homePath2, ".bgr", `${name}-err.txt`);
1669
+ Bun.write(stderrPath, "");
1670
+ const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1671
+ const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1672
+ env: buildManagedProcessEnv(Bun.env, finalEnv),
1673
+ cwd: finalDirectory,
1674
+ stdout: Bun.file(stdoutPath),
1675
+ stderr: Bun.file(stderrPath)
1676
+ });
1677
+ newProcess.unref();
1678
+ await sleep2(100);
1679
+ const pid = await findChildPid(newProcess.pid);
1680
+ await sleep2(400);
1681
+ return pid;
1682
+ }) ?? 0;
1683
+ await retryDatabaseOperation(() => insertProcess({
1684
+ pid: actualPid,
1685
+ workdir: finalDirectory,
1686
+ command: finalCommand,
1687
+ name,
1688
+ env: stringifyEnvString(finalEnv),
1689
+ configPath: finalConfigPath || "",
1690
+ stdout_path: stdoutPath,
1691
+ stderr_path: stderrPath
1692
+ }));
1693
+ if (!isInternalProcessName(name)) {
1694
+ await syncProcessWatcher(name, finalEnv);
1695
+ }
1696
+ announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1697
+ } finally {
1698
+ releaseOperationLock();
1200
1699
  }
1201
- const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
1202
- Bun.write(stdoutPath, "");
1203
- const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
1204
- Bun.write(stderrPath, "");
1205
- const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
1206
- const newProcess = Bun.spawn(getShellCommand(finalCommand), {
1207
- env: { ...Bun.env, ...finalEnv },
1208
- cwd: finalDirectory,
1209
- stdout: Bun.file(stdoutPath),
1210
- stderr: Bun.file(stderrPath)
1211
- });
1212
- newProcess.unref();
1213
- await sleep2(100);
1214
- const pid = await findChildPid(newProcess.pid);
1215
- await sleep2(400);
1216
- return pid;
1217
- }) ?? 0;
1218
- await retryDatabaseOperation(() => insertProcess({
1219
- pid: actualPid,
1220
- workdir: finalDirectory,
1221
- command: finalCommand,
1222
- name,
1223
- env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
1224
- configPath: finalConfigPath || "",
1225
- stdout_path: stdoutPath,
1226
- stderr_path: stderrPath
1227
- }));
1228
- announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
1229
1700
  }
1230
- var homePath2, run;
1231
- var init_run = __esm(() => {
1232
- init_db();
1233
- init_platform();
1234
- init_logger();
1235
- init_utils();
1236
- homePath2 = getHomeDir();
1237
- run = createMeasure2("run");
1238
- });
1701
+ // src/commands/envit.ts
1702
+ function parseEnvitArgs(args) {
1703
+ let directory;
1704
+ let configPath;
1705
+ let shell;
1706
+ let help = false;
1707
+ for (let i = 0;i < args.length; i++) {
1708
+ const arg = args[i];
1709
+ if (arg === "--help" || arg === "-h") {
1710
+ help = true;
1711
+ continue;
1712
+ }
1713
+ if (arg === "--directory") {
1714
+ directory = args[++i];
1715
+ if (!directory)
1716
+ error("Missing value for --directory.");
1717
+ continue;
1718
+ }
1719
+ if (arg.startsWith("--directory=")) {
1720
+ directory = arg.slice("--directory=".length);
1721
+ if (!directory)
1722
+ error("Missing value for --directory.");
1723
+ continue;
1724
+ }
1725
+ if (arg === "--config") {
1726
+ configPath = args[++i];
1727
+ if (!configPath)
1728
+ error("Missing value for --config.");
1729
+ continue;
1730
+ }
1731
+ if (arg.startsWith("--config=")) {
1732
+ configPath = arg.slice("--config=".length);
1733
+ if (!configPath)
1734
+ error("Missing value for --config.");
1735
+ continue;
1736
+ }
1737
+ if (arg === "--shell") {
1738
+ const value = args[++i];
1739
+ if (!value)
1740
+ error("Missing value for --shell.");
1741
+ shell = normalizeEnvitShell(value);
1742
+ continue;
1743
+ }
1744
+ if (arg.startsWith("--shell=")) {
1745
+ shell = normalizeEnvitShell(arg.slice("--shell=".length));
1746
+ continue;
1747
+ }
1748
+ if (!configPath) {
1749
+ configPath = arg;
1750
+ continue;
1751
+ }
1752
+ error(`Unexpected argument '${arg}'. envit prints shell export commands and does not run a child process.`);
1753
+ }
1754
+ return { directory, configPath, shell, help };
1755
+ }
1756
+ function normalizeEnvitShell(value) {
1757
+ const normalized = value.trim().toLowerCase();
1758
+ if (normalized === "powershell" || normalized === "pwsh" || normalized === "ps1")
1759
+ return "powershell";
1760
+ if (normalized === "cmd" || normalized === "bat")
1761
+ return "cmd";
1762
+ if (normalized === "sh" || normalized === "bash" || normalized === "zsh")
1763
+ return "sh";
1764
+ if (normalized === "json")
1765
+ return "json";
1766
+ error(`Unsupported shell '${value}'. Use powershell, cmd, sh, or json.`);
1767
+ }
1768
+ function detectEnvitShell() {
1769
+ if (process.platform === "win32") {
1770
+ return "powershell";
1771
+ }
1772
+ return "sh";
1773
+ }
1774
+ function escapePowerShell(value) {
1775
+ return `'${value.replace(/'/g, "''")}'`;
1776
+ }
1777
+ function escapeCmd(value) {
1778
+ return value.replace(/"/g, '""');
1779
+ }
1780
+ function escapeSh(value) {
1781
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1782
+ }
1783
+ function renderEnvitOutput(env, shell) {
1784
+ if (shell === "json") {
1785
+ return JSON.stringify(env, null, 2);
1786
+ }
1787
+ const lines = Object.entries(env).map(([key, value]) => {
1788
+ if (shell === "powershell") {
1789
+ return `$env:${key}=${escapePowerShell(value)}`;
1790
+ }
1791
+ if (shell === "cmd") {
1792
+ return `set "${key}=${escapeCmd(value)}"`;
1793
+ }
1794
+ return `export ${key}=${escapeSh(value)}`;
1795
+ });
1796
+ return lines.join(`
1797
+ `);
1798
+ }
1799
+ async function handleEnvit(options) {
1800
+ const cwd = options.directory || process.cwd();
1801
+ const configPath = options.configPath || ".config.toml";
1802
+ const shell = options.shell || detectEnvitShell();
1803
+ const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
1804
+ if (!exists) {
1805
+ error(`Config file '${configPath}' not found.`);
1806
+ }
1807
+ console.log(renderEnvitOutput(configEnv, shell));
1808
+ }
1809
+ // src/commands/inline.ts
1810
+ init_utils();
1811
+ function parseInlineArgs(args) {
1812
+ let directory;
1813
+ let configPath;
1814
+ let help = false;
1815
+ const commandArgs = [];
1816
+ for (let i = 0;i < args.length; i++) {
1817
+ const arg = args[i];
1818
+ if (commandArgs.length > 0) {
1819
+ commandArgs.push(arg);
1820
+ continue;
1821
+ }
1822
+ if (arg === "--") {
1823
+ commandArgs.push(...args.slice(i + 1));
1824
+ break;
1825
+ }
1826
+ if (arg === "--help" || arg === "-h") {
1827
+ help = true;
1828
+ continue;
1829
+ }
1830
+ if (arg === "--directory") {
1831
+ directory = args[++i];
1832
+ if (!directory)
1833
+ error("Missing value for --directory.");
1834
+ continue;
1835
+ }
1836
+ if (arg.startsWith("--directory=")) {
1837
+ directory = arg.slice("--directory=".length);
1838
+ if (!directory)
1839
+ error("Missing value for --directory.");
1840
+ continue;
1841
+ }
1842
+ if (arg === "--config") {
1843
+ configPath = args[++i];
1844
+ if (!configPath)
1845
+ error("Missing value for --config.");
1846
+ continue;
1847
+ }
1848
+ if (arg.startsWith("--config=")) {
1849
+ configPath = arg.slice("--config=".length);
1850
+ if (!configPath)
1851
+ error("Missing value for --config.");
1852
+ continue;
1853
+ }
1854
+ commandArgs.push(...args.slice(i));
1855
+ break;
1856
+ }
1857
+ return { directory, configPath, commandArgs, help };
1858
+ }
1859
+ async function handleInline(options) {
1860
+ const cwd = options.directory || process.cwd();
1861
+ const configPath = options.configPath || ".config.toml";
1862
+ const { configEnv, exists } = await loadConfigEnv(cwd, configPath);
1863
+ if (exists) {
1864
+ console.log(`Loaded config from ${configPath}`);
1865
+ } else {
1866
+ console.log(`Config file '${configPath}' not found, continuing without it.`);
1867
+ }
1868
+ if (options.commandArgs.length === 0) {
1869
+ error("Please provide a command to run. Example: bgrun inline -- bun run dev");
1870
+ }
1871
+ const proc = Bun.spawn(options.commandArgs, {
1872
+ cwd,
1873
+ env: buildManagedProcessEnv(Bun.env, configEnv),
1874
+ stdin: "inherit",
1875
+ stdout: "inherit",
1876
+ stderr: "inherit"
1877
+ });
1878
+ const exitCode = await proc.exited;
1879
+ process.exit(exitCode);
1880
+ }
1239
1881
 
1240
1882
  // src/api.ts
1241
- init_db();
1242
- init_platform();
1243
- init_run();
1244
1883
  init_utils();
1245
1884
  init_db();
1246
1885
  init_platform();
1247
- init_run();
1248
1886
  init_utils();
1249
1887
  var api_default = {
1250
1888
  db,
@@ -1285,11 +1923,31 @@ var api_default = {
1285
1923
  getProcessBatchResources,
1286
1924
  getProcessMemory,
1287
1925
  reconcileProcessPids,
1926
+ resolvePidWithPorts,
1288
1927
  handleRun,
1928
+ handleEnvit,
1929
+ parseEnvitArgs,
1930
+ renderEnvitOutput,
1931
+ handleInline,
1932
+ parseInlineArgs,
1933
+ ensureProcessWatcher,
1934
+ stopProcessWatcher,
1935
+ syncProcessWatcher,
1936
+ getGuardRestartCounts,
1937
+ getRecentGuardEvents,
1289
1938
  getVersion,
1290
1939
  calculateRuntime,
1291
1940
  parseEnvString,
1292
- validateDirectory
1941
+ parseCommandEnv,
1942
+ getDeclaredPort,
1943
+ validateDirectory,
1944
+ acquireProcessOperationLock,
1945
+ isProcessOperationLocked,
1946
+ stringifyEnvString,
1947
+ getWatcherProcessName,
1948
+ getWatchedProcessName,
1949
+ isWatcherProcessName,
1950
+ isInternalProcessName
1293
1951
  };
1294
1952
  export {
1295
1953
  waitForPortFree,
@@ -1297,36 +1955,55 @@ export {
1297
1955
  updateProcessPid,
1298
1956
  updateProcessEnv,
1299
1957
  terminateProcess,
1958
+ syncProcessWatcher,
1959
+ stringifyEnvString,
1960
+ stopProcessWatcher,
1300
1961
  saveTemplate,
1301
1962
  retryDatabaseOperation,
1963
+ resolvePidWithPorts,
1964
+ renderEnvitOutput,
1302
1965
  removeProcessByName,
1303
1966
  removeProcess,
1304
1967
  removeDependency,
1305
1968
  removeAllProcesses,
1306
1969
  reconcileProcessPids,
1307
1970
  readFileTail,
1971
+ parseInlineArgs,
1972
+ parseEnvitArgs,
1308
1973
  parseEnvString,
1974
+ parseCommandEnv,
1309
1975
  killProcessOnPort,
1310
1976
  isWindows,
1977
+ isWatcherProcessName,
1311
1978
  isProcessRunning,
1979
+ isProcessOperationLocked,
1980
+ isInternalProcessName,
1312
1981
  insertProcess,
1313
1982
  handleRun,
1983
+ handleInline,
1984
+ handleEnvit,
1985
+ getWatcherProcessName,
1986
+ getWatchedProcessName,
1314
1987
  getVersion,
1315
1988
  getStartOrder,
1316
1989
  getShellCommand,
1317
1990
  getRecentHistory,
1991
+ getRecentGuardEvents,
1318
1992
  getProcessPorts,
1319
1993
  getProcessMemory,
1320
1994
  getProcessHistory,
1321
1995
  getProcessBatchResources,
1322
1996
  getProcess,
1323
1997
  getHomeDir,
1998
+ getGuardRestartCounts,
1324
1999
  getDependencyGraph,
2000
+ getDeclaredPort,
1325
2001
  getDbInfo,
1326
2002
  getAllTemplates,
1327
2003
  getAllProcesses,
1328
2004
  findPidByPort,
1329
2005
  findChildPid,
2006
+ ensureProcessWatcher,
1330
2007
  ensureDir,
1331
2008
  deleteTemplate,
1332
2009
  api_default as default,
@@ -1335,5 +2012,6 @@ export {
1335
2012
  calculateRuntime,
1336
2013
  bgrHome,
1337
2014
  addHistoryEntry,
1338
- addDependency
2015
+ addDependency,
2016
+ acquireProcessOperationLock
1339
2017
  };