bgrun 3.4.0 → 3.7.1

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/image.png ADDED
Binary file
package/package.json CHANGED
@@ -1,56 +1,60 @@
1
- {
2
- "name": "bgrun",
3
- "version": "3.4.0",
4
- "description": "bgrun — A lightweight process manager for Bun",
5
- "type": "module",
6
- "main": "./src/api.ts",
7
- "exports": {
8
- ".": "./src/api.ts"
9
- },
10
- "bin": {
11
- "bgrun": "./dist/index.js"
12
- },
13
- "scripts": {
14
- "build": "bun run ./src/build.ts",
15
- "test": "bun test",
16
- "prepublishOnly": "bun run build"
17
- },
18
- "files": [
19
- "dist",
20
- "src",
21
- "dashboard/app",
22
- "README.md",
23
- "examples/bgr-startup.sh"
24
- ],
25
- "keywords": [
26
- "process-manager",
27
- "bun",
28
- "monitoring",
29
- "devops",
30
- "deployment",
31
- "background",
32
- "daemon"
33
- ],
34
- "author": "7flash",
35
- "license": "MIT",
36
- "repository": {
37
- "type": "git",
38
- "url": "https://github.com/Mements/bgr.git"
39
- },
40
- "devDependencies": {
41
- "bun-types": "latest"
42
- },
43
- "peerDependencies": {
44
- "typescript": "^5.0.0"
45
- },
46
- "dependencies": {
47
- "boxen": "^8.0.1",
48
- "chalk": "^5.4.1",
49
- "dedent": "^1.5.3",
50
- "melina": "^1.3.2",
51
- "sqlite-zod-orm": "^3.8.0"
52
- },
53
- "engines": {
54
- "bun": ">=1.0.0"
55
- }
56
- }
1
+ {
2
+ "name": "bgrun",
3
+ "version": "3.7.1",
4
+ "description": "bgrun — A lightweight process manager for Bun",
5
+ "type": "module",
6
+ "main": "./src/api.ts",
7
+ "exports": {
8
+ ".": "./src/api.ts"
9
+ },
10
+ "bin": {
11
+ "bgrun": "./dist/index.js"
12
+ },
13
+ "scripts": {
14
+ "build": "bun run ./src/build.ts",
15
+ "test": "bun test",
16
+ "prepublishOnly": "bun run build"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "dashboard/app",
22
+ "README.md",
23
+ "image.png",
24
+ "examples/bgr-startup.sh"
25
+ ],
26
+ "keywords": [
27
+ "process-manager",
28
+ "bun",
29
+ "monitoring",
30
+ "devops",
31
+ "deployment",
32
+ "background",
33
+ "daemon"
34
+ ],
35
+ "author": "7flash",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/7flash/bgrun.git"
40
+ },
41
+ "devDependencies": {
42
+ "bun-types": "latest"
43
+ },
44
+ "peerDependencies": {
45
+ "typescript": "^5.0.0"
46
+ },
47
+ "dependencies": {
48
+ "boxen": "^8.0.1",
49
+ "chalk": "^5.4.1",
50
+ "dedent": "^1.5.3",
51
+ "measure-fn": "^3.2.1",
52
+ "melina": "^2.2.1",
53
+ "react": "^19.2.4",
54
+ "react-dom": "^19.2.4",
55
+ "sqlite-zod-orm": "^3.8.0"
56
+ },
57
+ "engines": {
58
+ "bun": ">=1.0.0"
59
+ }
60
+ }
package/src/api.ts CHANGED
@@ -48,3 +48,16 @@ export { handleRun } from './commands/run'
48
48
 
49
49
  // --- Utilities ---
50
50
  export { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
51
+
52
+ // --- Default Export (namespace style) ---
53
+ import { getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation, getDbInfo, dbPath, bgrHome } from './db'
54
+ import { isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchMemory, getProcessMemory } from './platform'
55
+ import { handleRun } from './commands/run'
56
+ import { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
57
+
58
+ export default {
59
+ getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation, getDbInfo, dbPath, bgrHome,
60
+ isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows, getProcessBatchMemory, getProcessMemory,
61
+ handleRun,
62
+ getVersion, calculateRuntime, parseEnvString, validateDirectory,
63
+ }
@@ -5,6 +5,7 @@ import { getAllProcesses } from "../db";
5
5
  import { announce } from "../logger";
6
6
  import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
7
7
  import { getProcessPorts, getProcessBatchMemory } from "../platform";
8
+ import { measure } from "measure-fn";
8
9
 
9
10
  function formatMemory(bytes: number): string {
10
11
  if (bytes === 0) return '-';
@@ -1,4 +1,4 @@
1
- import { CommandOptions } from "../types";
1
+ import type { CommandOptions } from "../types";
2
2
  import { getProcess, removeProcessByName, retryDatabaseOperation, insertProcess } from "../db";
3
3
  import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree } from "../platform";
4
4
  import { error, announce } from "../logger";
@@ -7,8 +7,10 @@ import { parseConfigFile } from "../config";
7
7
  import { $ } from "bun";
8
8
  import { sleep } from "bun";
9
9
  import { join } from "path";
10
+ import { createMeasure } from "measure-fn";
10
11
 
11
12
  const homePath = getHomeDir();
13
+ const run = createMeasure('run');
12
14
 
13
15
  export async function handleRun(options: CommandOptions) {
14
16
  const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
@@ -24,18 +26,20 @@ export async function handleRun(options: CommandOptions) {
24
26
  if (!require('fs').existsSync(require('path').join(finalDirectory, '.git'))) {
25
27
  error(`Cannot --fetch: '${finalDirectory}' is not a Git repository.`);
26
28
  }
27
- try {
28
- await $`git fetch origin`;
29
- const localHash = (await $`git rev-parse HEAD`.text()).trim();
30
- const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
31
-
32
- if (localHash !== remoteHash) {
33
- await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
34
- announce("📥 Pulled latest changes", "Git Update");
29
+ await run.measure(`Git fetch "${name}"`, async () => {
30
+ try {
31
+ await $`git fetch origin`;
32
+ const localHash = (await $`git rev-parse HEAD`.text()).trim();
33
+ const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
34
+
35
+ if (localHash !== remoteHash) {
36
+ await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
37
+ announce("📥 Pulled latest changes", "Git Update");
38
+ }
39
+ } catch (err) {
40
+ error(`Failed to pull latest changes: ${err}`);
35
41
  }
36
- } catch (err) {
37
- error(`Failed to pull latest changes: ${err}`);
38
- }
42
+ });
39
43
  }
40
44
 
41
45
  const isRunning = await isProcessRunning(existingProcess.pid);
@@ -50,23 +54,26 @@ export async function handleRun(options: CommandOptions) {
50
54
  }
51
55
 
52
56
  if (isRunning) {
53
- await terminateProcess(existingProcess.pid);
54
- announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
57
+ await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
58
+ await terminateProcess(existingProcess.pid);
59
+ announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
60
+ });
55
61
  }
56
62
 
57
63
  // Kill anything still on the ports the old process was using
58
- for (const port of detectedPorts) {
59
- await killProcessOnPort(port);
60
- }
61
-
62
- // Wait for all detected ports to free up
63
- for (const port of detectedPorts) {
64
- const freed = await waitForPortFree(port, 5000);
65
- if (!freed) {
66
- // Retry kill and wait once more
67
- await killProcessOnPort(port);
68
- await waitForPortFree(port, 3000);
69
- }
64
+ if (detectedPorts.length > 0) {
65
+ await run.measure(`Port cleanup [${detectedPorts.join(', ')}]`, async () => {
66
+ for (const port of detectedPorts) {
67
+ await killProcessOnPort(port);
68
+ }
69
+ for (const port of detectedPorts) {
70
+ const freed = await waitForPortFree(port, 5000);
71
+ if (!freed) {
72
+ await killProcessOnPort(port);
73
+ await waitForPortFree(port, 3000);
74
+ }
75
+ }
76
+ });
70
77
  }
71
78
 
72
79
  await retryDatabaseOperation(() =>
@@ -97,12 +104,17 @@ export async function handleRun(options: CommandOptions) {
97
104
  const fullConfigPath = join(finalDirectory, finalConfigPath);
98
105
 
99
106
  if (await Bun.file(fullConfigPath).exists()) {
100
- try {
101
- const newConfigEnv = await parseConfigFile(fullConfigPath);
102
- finalEnv = { ...finalEnv, ...newConfigEnv };
107
+ const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
108
+ try {
109
+ return await parseConfigFile(fullConfigPath);
110
+ } catch (err: any) {
111
+ console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
112
+ return null;
113
+ }
114
+ });
115
+ if (configEnv) {
116
+ finalEnv = { ...finalEnv, ...configEnv };
103
117
  console.log(`Loaded config from ${finalConfigPath}`);
104
- } catch (err: any) {
105
- console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
106
118
  }
107
119
  } else {
108
120
  console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
@@ -114,20 +126,23 @@ export async function handleRun(options: CommandOptions) {
114
126
  const stderrPath = stderr || existingProcess?.stderr_path || join(homePath, ".bgr", `${name}-err.txt`);
115
127
  Bun.write(stderrPath, '');
116
128
 
117
- const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
118
- env: { ...Bun.env, ...finalEnv },
119
- cwd: finalDirectory,
120
- stdout: Bun.file(stdoutPath),
121
- stderr: Bun.file(stderrPath),
122
- });
123
-
124
- newProcess.unref();
125
- // Give shell a moment to spawn child, then find PID before shell exits
126
- await sleep(100);
127
- // Find the actual child PID (shell wrapper exits immediately after spawning)
128
- const actualPid = await findChildPid(newProcess.pid);
129
- // Wait more for subprocess to initialize
130
- await sleep(400);
129
+ const actualPid = await run.measure(`Spawn "${name}" → ${finalCommand}`, async () => {
130
+ const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
131
+ env: { ...Bun.env, ...finalEnv },
132
+ cwd: finalDirectory,
133
+ stdout: Bun.file(stdoutPath),
134
+ stderr: Bun.file(stderrPath),
135
+ });
136
+
137
+ newProcess.unref();
138
+ // Give shell a moment to spawn child, then find PID before shell exits
139
+ await sleep(100);
140
+ // Find the actual child PID (shell wrapper exits immediately after spawning)
141
+ const pid = await findChildPid(newProcess.pid);
142
+ // Wait more for subprocess to initialize
143
+ await sleep(400);
144
+ return pid;
145
+ }) ?? 0;
131
146
 
132
147
  await retryDatabaseOperation(() =>
133
148
  insertProcess({
@@ -147,5 +162,3 @@ export async function handleRun(options: CommandOptions) {
147
162
  "Process Started"
148
163
  );
149
164
  }
150
-
151
-
package/src/db.ts CHANGED
@@ -2,6 +2,7 @@ import { Database, z } from "sqlite-zod-orm";
2
2
  import { getHomeDir, ensureDir } from "./platform";
3
3
  import { join } from "path";
4
4
  import { sleep } from "bun";
5
+ import { existsSync, copyFileSync } from "fs";
5
6
 
6
7
  // =============================================================================
7
8
  // SCHEMA (inline — single table, no need for a separate file)
@@ -27,11 +28,24 @@ export type Process = z.infer<typeof ProcessSchema> & { id: number };
27
28
 
28
29
  const homePath = getHomeDir();
29
30
  const bgrDir = join(homePath, ".bgr");
30
- const dbName = process.env.DB_NAME ?? "bgr";
31
- export const dbPath = join(bgrDir, `${dbName}_v2.sqlite`);
32
- export const bgrHome = bgrDir;
33
31
  ensureDir(bgrDir);
34
32
 
33
+ // DB filename: configurable via BGRUN_DB env, default "bgrun.sqlite"
34
+ const dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
35
+ export const dbPath = join(bgrDir, dbFilename);
36
+ export const bgrHome = bgrDir;
37
+
38
+ // Auto-migration: if new DB doesn't exist but old one does, copy it over
39
+ const legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
40
+ if (!existsSync(dbPath) && existsSync(legacyDbPath)) {
41
+ try {
42
+ copyFileSync(legacyDbPath, dbPath);
43
+ console.log(`[bgrun] Migrated database: ${legacyDbPath} → ${dbPath}`);
44
+ } catch (e) {
45
+ // Migration failed — start fresh
46
+ }
47
+ }
48
+
35
49
  export const db = new Database(dbPath, {
36
50
  process: ProcessSchema,
37
51
  }, {
@@ -90,6 +104,14 @@ export function removeProcessByName(name: string) {
90
104
  }
91
105
  }
92
106
 
107
+ /** Update the stored PID for a process (used by PID reconciliation) */
108
+ export function updateProcessPid(name: string, newPid: number) {
109
+ const proc = db.process.select().where({ name }).limit(1).get();
110
+ if (proc) {
111
+ db.process.update(proc.id, { pid: newPid });
112
+ }
113
+ }
114
+
93
115
  export function removeAllProcesses() {
94
116
  const all = db.process.select().all();
95
117
  for (const p of all) {
@@ -105,8 +127,8 @@ export function getDbInfo() {
105
127
  return {
106
128
  dbPath,
107
129
  bgrHome,
108
- dbName,
109
- exists: require('fs').existsSync(dbPath),
130
+ dbFilename,
131
+ exists: existsSync(dbPath),
110
132
  };
111
133
  }
112
134
 
package/src/index.ts CHANGED
@@ -10,13 +10,21 @@ import { showLogs } from "./commands/logs";
10
10
  import { showDetails } from "./commands/details";
11
11
  import type { CommandOptions } from "./types";
12
12
  import { error, announce } from "./logger";
13
- import { startServer } from "./server";
13
+ // startServer is dynamically imported only when --_serve is used
14
+ // to avoid loading melina (which has side-effects) on every bgrun command
14
15
  import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree } from "./platform";
15
16
  import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation, getDbInfo } from "./db";
16
17
  import dedent from "dedent";
17
18
  import chalk from "chalk";
18
19
  import { join } from "path";
19
20
  import { sleep } from "bun";
21
+ import { configure } from "measure-fn";
22
+
23
+ if (!Bun.argv.includes("--_serve")) {
24
+ if (!Bun.env.MEASURE_SILENT) {
25
+ configure({ silent: true });
26
+ }
27
+ }
20
28
 
21
29
  async function showHelp() {
22
30
  const usage = dedent`
@@ -31,7 +39,9 @@ async function showHelp() {
31
39
  bgrun [name] Show details for a process
32
40
  bgrun --dashboard Launch web dashboard (managed by bgrun)
33
41
  bgrun --restart [name] Restart a process
42
+ bgrun --restart-all Restart ALL registered processes
34
43
  bgrun --stop [name] Stop a process (keep in registry)
44
+ bgrun --stop-all Stop ALL running processes
35
45
  bgrun --delete [name] Delete a process
36
46
  bgrun --clean Remove all stopped processes
37
47
  bgrun --nuke Delete ALL processes
@@ -79,7 +89,9 @@ async function run() {
79
89
  delete: { type: 'boolean' },
80
90
  nuke: { type: 'boolean' },
81
91
  restart: { type: 'boolean' },
92
+ "restart-all": { type: 'boolean' },
82
93
  stop: { type: 'boolean' },
94
+ "stop-all": { type: 'boolean' },
83
95
  clean: { type: 'boolean' },
84
96
  json: { type: 'boolean' },
85
97
  logs: { type: 'boolean' },
@@ -105,6 +117,7 @@ async function run() {
105
117
  // Port is NOT passed explicitly — Melina auto-detects from BUN_PORT env
106
118
  // or defaults to 3000 with fallback to next available port.
107
119
  if (values['_serve']) {
120
+ const { startServer } = await import("./server");
108
121
  await startServer();
109
122
  return;
110
123
  }
@@ -121,7 +134,15 @@ async function run() {
121
134
  // Check if dashboard is already running
122
135
  const existing = getProcess(dashboardName);
123
136
  if (existing && await isProcessRunning(existing.pid)) {
124
- const existingPorts = await getProcessPorts(existing.pid);
137
+ // The stored PID may be the shell wrapper (cmd.exe), not the actual bun process
138
+ // Try the stored PID first, then traverse the process tree to find the real one
139
+ let existingPorts = await getProcessPorts(existing.pid);
140
+ if (existingPorts.length === 0) {
141
+ const childPid = await findChildPid(existing.pid);
142
+ if (childPid !== existing.pid) {
143
+ existingPorts = await getProcessPorts(childPid);
144
+ }
145
+ }
125
146
  const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : '(detecting...)';
126
147
  announce(
127
148
  `Dashboard is already running (PID ${existing.pid})\n\n` +
@@ -244,7 +265,7 @@ async function run() {
244
265
  Version: ${chalk.cyan(version)}
245
266
  BGR Home: ${chalk.yellow(info.bgrHome)}
246
267
  DB Path: ${chalk.yellow(info.dbPath)}
247
- DB Name: ${info.dbName}
268
+ DB File: ${info.dbFilename}
248
269
  DB Exists: ${info.exists ? chalk.green('✓') : chalk.red('✗')}
249
270
  Platform: ${process.platform}
250
271
  Bun: ${Bun.version}
@@ -263,6 +284,57 @@ async function run() {
263
284
  return;
264
285
  }
265
286
 
287
+ // Restart all registered processes
288
+ if (values['restart-all']) {
289
+ const { getAllProcesses } = await import('./db');
290
+ const all = getAllProcesses();
291
+ if (all.length === 0) {
292
+ error('No processes registered.');
293
+ return;
294
+ }
295
+ console.log(chalk.bold(`\n Restarting ${all.length} processes...\n`));
296
+ for (const proc of all) {
297
+ try {
298
+ console.log(chalk.yellow(` ↻ Restarting ${proc.name}...`));
299
+ await handleRun({
300
+ action: 'run',
301
+ name: proc.name,
302
+ force: true,
303
+ remoteName: '',
304
+ });
305
+ } catch (err: any) {
306
+ console.error(chalk.red(` ✗ Failed to restart ${proc.name}: ${err.message}`));
307
+ }
308
+ }
309
+ console.log(chalk.green(`\n ✓ All processes restarted.\n`));
310
+ return;
311
+ }
312
+
313
+ // Stop all running processes
314
+ if (values['stop-all']) {
315
+ const { getAllProcesses } = await import('./db');
316
+ const all = getAllProcesses();
317
+ if (all.length === 0) {
318
+ error('No processes registered.');
319
+ return;
320
+ }
321
+ console.log(chalk.bold(`\n Stopping ${all.length} processes...\n`));
322
+ for (const proc of all) {
323
+ try {
324
+ if (await isProcessRunning(proc.pid)) {
325
+ console.log(chalk.yellow(` ■ Stopping ${proc.name} (PID ${proc.pid})...`));
326
+ await handleStop(proc.name);
327
+ } else {
328
+ console.log(chalk.gray(` ○ ${proc.name} already stopped`));
329
+ }
330
+ } catch (err: any) {
331
+ console.error(chalk.red(` ✗ Failed to stop ${proc.name}: ${err.message}`));
332
+ }
333
+ }
334
+ console.log(chalk.green(`\n ✓ All processes stopped.\n`));
335
+ return;
336
+ }
337
+
266
338
  const name = (values.name as string) || positionals[0];
267
339
 
268
340
  // Delete