bgrun 3.12.0 → 3.12.2

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/src/index.ts CHANGED
@@ -12,7 +12,7 @@ import type { CommandOptions } from "./types";
12
12
  import { error, announce } from "./logger";
13
13
  // startServer is dynamically imported only when --_serve is used
14
14
  // to avoid loading melina (which has side-effects) on every bgrun command
15
- import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree, isPortFree } from "./platform";
15
+ import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree, isPortFree, findPidByPort } from "./platform";
16
16
  import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation, getDbInfo } from "./db";
17
17
  import dedent from "dedent";
18
18
  import chalk from "chalk";
@@ -26,6 +26,48 @@ if (!Bun.argv.includes("--_serve")) {
26
26
  }
27
27
  }
28
28
 
29
+ /**
30
+ * Redirect console.log/warn/error to log files when running detached.
31
+ * The parent spawner passes file paths via BGR_STDOUT/BGR_STDERR env vars.
32
+ * Appends timestamped lines so `bgrun <name> --logs` shows real output.
33
+ */
34
+ function redirectConsoleToFiles() {
35
+ const stdoutPath = Bun.env.BGR_STDOUT;
36
+ const stderrPath = Bun.env.BGR_STDERR;
37
+ if (!stdoutPath && !stderrPath) return; // Not detached, keep normal console
38
+
39
+ const { appendFileSync } = require('fs');
40
+
41
+ // Strip ANSI escape codes for clean log files
42
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
43
+
44
+ const timestamp = () => new Date().toISOString().replace('T', ' ').substring(0, 19);
45
+
46
+ if (stdoutPath) {
47
+ const origLog = console.log;
48
+ const origWarn = console.warn;
49
+ console.log = (...args: any[]) => {
50
+ const line = `[${timestamp()}] ${stripAnsi(args.map(String).join(' '))}\n`;
51
+ try { appendFileSync(stdoutPath, line); } catch { }
52
+ origLog.apply(console, args); // Also keep original (goes to /dev/null when detached, but useful if attached)
53
+ };
54
+ console.warn = (...args: any[]) => {
55
+ const line = `[${timestamp()}] WARN: ${stripAnsi(args.map(String).join(' '))}\n`;
56
+ try { appendFileSync(stdoutPath, line); } catch { }
57
+ origWarn.apply(console, args);
58
+ };
59
+ }
60
+
61
+ if (stderrPath) {
62
+ const origError = console.error;
63
+ console.error = (...args: any[]) => {
64
+ const line = `[${timestamp()}] ERROR: ${stripAnsi(args.map(String).join(' '))}\n`;
65
+ try { appendFileSync(stderrPath, line); } catch { }
66
+ origError.apply(console, args);
67
+ };
68
+ }
69
+ }
70
+
29
71
  async function showHelp() {
30
72
  const usage = dedent`
31
73
  ${chalk.bold('bgrun — Bun Background Runner')}
@@ -120,6 +162,9 @@ async function run() {
120
162
  // Port is NOT passed explicitly — Melina auto-detects from BUN_PORT env
121
163
  // or defaults to 3000 with fallback to next available port.
122
164
  if (values['_serve']) {
165
+ // Redirect console output to log files when running detached
166
+ // The spawner passes paths via BGR_STDOUT/BGR_STDERR env vars
167
+ redirectConsoleToFiles();
123
168
  const { startServer } = await import("./server");
124
169
  await startServer();
125
170
  return;
@@ -127,6 +172,8 @@ async function run() {
127
172
 
128
173
  // Internal: actually run the guard loop (spawned by --guard)
129
174
  if (values['_guard-loop']) {
175
+ // Redirect console output to log files when running detached
176
+ redirectConsoleToFiles();
130
177
  const { startGuardLoop } = await import("./guard");
131
178
  const intervalStr = positionals[0];
132
179
  const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
@@ -193,10 +240,13 @@ async function run() {
193
240
  await Bun.write(stderrPath, '');
194
241
 
195
242
  // Pass BUN_PORT env var only if user explicitly requested a port
196
- const spawnEnv = { ...Bun.env };
243
+ const spawnEnv: Record<string, string> = { ...Bun.env } as any;
197
244
  if (requestedPort) {
198
245
  spawnEnv.BUN_PORT = requestedPort;
199
246
  }
247
+ // Pass log paths so the detached process can redirect its own console output
248
+ spawnEnv.BGR_STDOUT = stdoutPath;
249
+ spawnEnv.BGR_STDERR = stderrPath;
200
250
 
201
251
  // Resolve the target port: --port flag > BUN_PORT env > default 3000
202
252
  const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || '3000');
@@ -216,16 +266,18 @@ async function run() {
216
266
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
217
267
  env: spawnEnv,
218
268
  cwd: bgrDir,
219
- stdout: Bun.file(stdoutPath),
220
- stderr: Bun.file(stderrPath),
221
- });
269
+ stdout: "ignore",
270
+ stderr: "ignore",
271
+ detached: true, // Windows: new process group outside parent's Job Object — survives terminal close
272
+ } as any);
222
273
 
223
274
  newProcess.unref();
224
275
 
225
- // Resolve the actual child PID by traversing the process tree
226
- // (cmd.exe bun.exe), then detect which port it bound
276
+ // With detached: cmd.exe wrapper exits immediately, so findChildPid won't work.
277
+ // Instead, wait for the server to bind a port and find the PID from there.
227
278
  await sleep(2000); // Give the server time to start and bind a port
228
- const actualPid = await findChildPid(newProcess.pid);
279
+ const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || '3000');
280
+ const actualPid = await findPidByPort(resolvedPort, 10000) ?? await findChildPid(newProcess.pid);
229
281
 
230
282
  // Detect the port the server actually bound to
231
283
  let actualPort: number | null = null;
@@ -310,15 +362,27 @@ async function run() {
310
362
  await Bun.write(stderrPath, '');
311
363
 
312
364
  const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
313
- env: { ...Bun.env },
365
+ env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
314
366
  cwd: bgrDir,
315
- stdout: Bun.file(stdoutPath),
316
- stderr: Bun.file(stderrPath),
317
- });
367
+ stdout: "ignore",
368
+ stderr: "ignore",
369
+ detached: true, // Windows: new process group outside parent's Job Object — survives terminal close
370
+ } as any);
318
371
 
319
372
  newProcess.unref();
320
373
  await sleep(1000);
321
- const actualPid = await findChildPid(newProcess.pid);
374
+ // With detached: cmd.exe exits immediately. Search for the guard by command line.
375
+ let actualPid = await findChildPid(newProcess.pid);
376
+ if (!(await isProcessRunning(actualPid))) {
377
+ // cmd.exe already died — search for the bun process running --_guard-loop
378
+ const { psExec: ps } = await import('./platform');
379
+ const result = ps(
380
+ `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`,
381
+ 3000
382
+ );
383
+ const foundPid = parseInt(result.trim());
384
+ if (!isNaN(foundPid) && foundPid > 0) actualPid = foundPid;
385
+ }
322
386
 
323
387
  await retryDatabaseOperation(() =>
324
388
  insertProcess({
@@ -510,6 +574,15 @@ async function run() {
510
574
  return;
511
575
  }
512
576
 
577
+ // Explicit "list" command
578
+ if (name === 'list') {
579
+ await showAll({
580
+ json: values.json as boolean | undefined,
581
+ filter: values.filter as string | undefined
582
+ });
583
+ return;
584
+ }
585
+
513
586
  // List or Run or Details
514
587
  if (name) {
515
588
  if (!values.command && !values.directory) {
@@ -541,5 +614,10 @@ async function run() {
541
614
  }
542
615
 
543
616
  run().catch(err => {
544
- error(err);
617
+ // BgrunError was already printed by error() — just exit
618
+ // For unexpected errors, print and exit
619
+ if (err.name !== 'BgrunError') {
620
+ console.error(err);
621
+ }
622
+ process.exit(1);
545
623
  });