bgrun 3.3.1 → 3.3.3
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/README.md +126 -144
- package/dist/index.js +29 -131
- package/package.json +56 -60
- package/src/api.ts +4 -0
- package/src/commands/cleanup.ts +2 -7
- package/src/commands/list.ts +2 -2
- package/src/commands/watch.ts +5 -5
- package/src/config.ts +1 -1
- package/src/server.ts +4 -2
- package/src/table.ts +6 -6
- package/src/types.ts +0 -13
- package/src/utils.ts +2 -5
- package/src/schema.ts +0 -2
- package/src/version.macro.ts +0 -17
package/README.md
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# ⚡ bgrun
|
|
4
4
|
|
|
5
|
-
**Background Runner — a modern process manager built on Bun**
|
|
5
|
+
**Bun Background Runner — a modern process manager built on Bun**
|
|
6
6
|
|
|
7
|
-
[](https://www.npmjs.com/package/bgrun)
|
|
8
8
|
[](https://bun.sh/)
|
|
9
|
-
[](./LICENSE)
|
|
10
10
|
|
|
11
11
|
Start, stop, restart, and monitor any process — from dev servers to Docker containers.
|
|
12
12
|
Zero config. One command. Beautiful dashboard included.
|
|
13
13
|
|
|
14
14
|
```
|
|
15
|
-
bun install -g
|
|
15
|
+
bun install -g bgrun
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
## Why
|
|
22
|
+
## Why bgrun?
|
|
23
23
|
|
|
24
|
-
| Feature | PM2 |
|
|
25
|
-
|
|
24
|
+
| Feature | PM2 | bgrun |
|
|
25
|
+
|---------|-----|-------|
|
|
26
26
|
| Runtime | Node.js | Bun (5× faster startup) |
|
|
27
|
-
| Install | `npm i -g pm2` (50+ deps) | `bun i -g
|
|
27
|
+
| Install | `npm i -g pm2` (50+ deps) | `bun i -g bgrun` (minimal deps) |
|
|
28
28
|
| Config format | JSON / JS / YAML | TOML (or none at all) |
|
|
29
|
-
| Dashboard | `pm2 monit` (TUI) | `
|
|
29
|
+
| Dashboard | `pm2 monit` (TUI) | `bgrun --dashboard` (full web UI) |
|
|
30
30
|
| Language support | Any | Any |
|
|
31
31
|
| Docker-aware | ❌ | ✅ detects container status |
|
|
32
32
|
| Port management | Manual | Auto-detect & cleanup |
|
|
@@ -34,25 +34,27 @@ bun install -g bgr
|
|
|
34
34
|
| Programmatic API | ✅ | ✅ (first-class TypeScript) |
|
|
35
35
|
| Process persistence | ✅ | ✅ (SQLite) |
|
|
36
36
|
|
|
37
|
+
> **Note:** The CLI is available as both `bgrun` and `bgr` (alias). All examples below use `bgrun`.
|
|
38
|
+
|
|
37
39
|
---
|
|
38
40
|
|
|
39
41
|
## Quick Start
|
|
40
42
|
|
|
41
43
|
```bash
|
|
42
44
|
# Install globally
|
|
43
|
-
bun install -g
|
|
45
|
+
bun install -g bgrun
|
|
44
46
|
|
|
45
47
|
# Start a process
|
|
46
|
-
|
|
48
|
+
bgrun --name my-api --directory ./my-project --command "bun run server.ts"
|
|
47
49
|
|
|
48
50
|
# List all processes
|
|
49
|
-
|
|
51
|
+
bgrun
|
|
50
52
|
|
|
51
53
|
# Open the web dashboard
|
|
52
|
-
|
|
54
|
+
bgrun --dashboard
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
That's it.
|
|
57
|
+
That's it. bgrun tracks the PID, captures stdout/stderr, detects the port, and survives terminal close.
|
|
56
58
|
|
|
57
59
|
---
|
|
58
60
|
|
|
@@ -77,7 +79,7 @@ That's it. BGR tracks the PID, captures stdout/stderr, detects the port, and sur
|
|
|
77
79
|
### Starting a process
|
|
78
80
|
|
|
79
81
|
```bash
|
|
80
|
-
|
|
82
|
+
bgrun --name my-api \
|
|
81
83
|
--directory ~/projects/my-api \
|
|
82
84
|
--command "bun run server.ts"
|
|
83
85
|
```
|
|
@@ -85,34 +87,34 @@ bgr --name my-api \
|
|
|
85
87
|
Short form — if you're already *in* the project directory:
|
|
86
88
|
|
|
87
89
|
```bash
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
+
bgrun --name my-api --command "bun run server.ts"
|
|
91
|
+
# bgrun uses current directory by default
|
|
90
92
|
```
|
|
91
93
|
|
|
92
94
|
### Listing processes
|
|
93
95
|
|
|
94
96
|
```bash
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
bgrun # Pretty table
|
|
98
|
+
bgrun --json # Machine-readable JSON
|
|
99
|
+
bgrun --filter api # Filter by group (BGR_GROUP env)
|
|
98
100
|
```
|
|
99
101
|
|
|
100
102
|
### Viewing a process
|
|
101
103
|
|
|
102
104
|
```bash
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
bgrun my-api # Show status, PID, port, runtime, command
|
|
106
|
+
bgrun my-api --logs # Show stdout + stderr interleaved
|
|
107
|
+
bgrun my-api --logs --log-stdout --lines 50 # Last 50 stdout lines only
|
|
106
108
|
```
|
|
107
109
|
|
|
108
110
|
### Stopping, restarting, deleting
|
|
109
111
|
|
|
110
112
|
```bash
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
bgrun --stop my-api # Graceful stop (SIGTERM → SIGKILL)
|
|
114
|
+
bgrun --restart my-api # Stop then start again with same command
|
|
115
|
+
bgrun --delete my-api # Stop and remove from database
|
|
116
|
+
bgrun --clean # Remove all stopped processes
|
|
117
|
+
bgrun --nuke # ☠️ Delete everything
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
### Force restart
|
|
@@ -120,7 +122,7 @@ bgr --nuke # ☠️ Delete everything
|
|
|
120
122
|
When a process is stuck or its port is orphaned:
|
|
121
123
|
|
|
122
124
|
```bash
|
|
123
|
-
|
|
125
|
+
bgrun --name my-api --command "bun run server.ts" --force
|
|
124
126
|
```
|
|
125
127
|
|
|
126
128
|
`--force` will:
|
|
@@ -134,10 +136,10 @@ bgr --name my-api --command "bun run server.ts" --force
|
|
|
134
136
|
|
|
135
137
|
## Dashboard
|
|
136
138
|
|
|
137
|
-
|
|
139
|
+
bgrun ships with a built-in web dashboard for managing all your processes visually.
|
|
138
140
|
|
|
139
141
|
```bash
|
|
140
|
-
|
|
142
|
+
bgrun --dashboard
|
|
141
143
|
```
|
|
142
144
|
|
|
143
145
|
The dashboard provides:
|
|
@@ -145,7 +147,7 @@ The dashboard provides:
|
|
|
145
147
|
- **Start/stop/restart/delete** actions with one click
|
|
146
148
|
- **Log viewer** with monospace display and auto-scroll
|
|
147
149
|
- **Process detail drawer** with stdout/stderr tabs
|
|
148
|
-
- **Auto-refresh** every
|
|
150
|
+
- **Auto-refresh** every 5 seconds
|
|
149
151
|
|
|
150
152
|
### Dashboard port selection
|
|
151
153
|
|
|
@@ -153,21 +155,21 @@ The dashboard uses [Melina.js](https://github.com/7flash/melina.js) for serving
|
|
|
153
155
|
|
|
154
156
|
| Scenario | Behavior |
|
|
155
157
|
|----------|----------|
|
|
156
|
-
| `
|
|
157
|
-
| `BUN_PORT=4000
|
|
158
|
-
| `
|
|
158
|
+
| `bgrun --dashboard` | Starts on port 3000. If busy, auto-falls back to 3001, 3002, etc. |
|
|
159
|
+
| `BUN_PORT=4000 bgrun --dashboard` | Starts on port 4000. Fails with error if port is busy. |
|
|
160
|
+
| `bgrun --dashboard --port 5000` | Same as `BUN_PORT=5000` — explicit, no fallback. |
|
|
159
161
|
| Dashboard already running | Prints current URL and PID instead of starting a second instance. |
|
|
160
162
|
|
|
161
|
-
The actual port is always detected from the running process and displayed correctly in `
|
|
163
|
+
The actual port is always detected from the running process and displayed correctly in `bgrun` output.
|
|
162
164
|
|
|
163
165
|
---
|
|
164
166
|
|
|
165
167
|
## File Watching
|
|
166
168
|
|
|
167
|
-
For development,
|
|
169
|
+
For development, bgrun can watch for file changes and auto-restart:
|
|
168
170
|
|
|
169
171
|
```bash
|
|
170
|
-
|
|
172
|
+
bgrun --name frontend \
|
|
171
173
|
--directory ~/projects/frontend \
|
|
172
174
|
--command "bun run dev" \
|
|
173
175
|
--watch
|
|
@@ -176,7 +178,7 @@ bgr --name frontend \
|
|
|
176
178
|
This monitors the working directory for changes and restarts the process when files are modified. Combine with `--force` to ensure clean restarts:
|
|
177
179
|
|
|
178
180
|
```bash
|
|
179
|
-
|
|
181
|
+
bgrun --name api \
|
|
180
182
|
--command "bun run server.ts" \
|
|
181
183
|
--watch \
|
|
182
184
|
--force \
|
|
@@ -187,20 +189,20 @@ bgr --name api \
|
|
|
187
189
|
|
|
188
190
|
## Port Handling
|
|
189
191
|
|
|
190
|
-
|
|
192
|
+
bgrun automatically detects which TCP ports a process is listening on by querying the OS. This means:
|
|
191
193
|
|
|
192
|
-
- **No port configuration needed** —
|
|
193
|
-
- **No environment variable assumptions** —
|
|
194
|
+
- **No port configuration needed** — bgrun discovers ports from `netstat`
|
|
195
|
+
- **No environment variable assumptions** — bgrun doesn't guess `PORT` or `BUN_PORT`
|
|
194
196
|
- **Clean restarts** — `--force` kills all orphaned port bindings before restarting
|
|
195
|
-
- **Accurate display** — the port shown in `
|
|
197
|
+
- **Accurate display** — the port shown in `bgrun` output is the *actual* bound port
|
|
196
198
|
|
|
197
199
|
### How it works
|
|
198
200
|
|
|
199
201
|
```
|
|
200
|
-
1.
|
|
202
|
+
1. bgrun spawns your process
|
|
201
203
|
2. Process starts and binds to a port (however it wants)
|
|
202
|
-
3.
|
|
203
|
-
4.
|
|
204
|
+
3. bgrun queries `netstat -ano` (Windows) or `ss -tlnp` (Linux)
|
|
205
|
+
4. bgrun finds all TCP LISTEN ports for the process PID
|
|
204
206
|
5. These ports are displayed in the table and used for cleanup
|
|
205
207
|
```
|
|
206
208
|
|
|
@@ -209,7 +211,7 @@ BGR automatically detects which TCP ports a process is listening on by querying
|
|
|
209
211
|
If you `--force` restart a process and its old port is still held by a zombie:
|
|
210
212
|
|
|
211
213
|
```
|
|
212
|
-
1.
|
|
214
|
+
1. bgrun detects ports held by the old PID
|
|
213
215
|
2. Sends SIGTERM to the old process
|
|
214
216
|
3. Kills any remaining processes on those ports
|
|
215
217
|
4. Waits for ports to become free (up to 5 seconds)
|
|
@@ -220,36 +222,36 @@ If you `--force` restart a process and its old port is still held by a zombie:
|
|
|
220
222
|
|
|
221
223
|
## Docker Integration
|
|
222
224
|
|
|
223
|
-
|
|
225
|
+
bgrun can manage Docker containers alongside regular processes:
|
|
224
226
|
|
|
225
227
|
```bash
|
|
226
228
|
# Start a Postgres container
|
|
227
|
-
|
|
229
|
+
bgrun --name postgres \
|
|
228
230
|
--command "docker run --name bgr-postgres -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:16"
|
|
229
231
|
|
|
230
232
|
# Start a Redis container
|
|
231
|
-
|
|
233
|
+
bgrun --name redis \
|
|
232
234
|
--command "docker run --name bgr-redis -p 6379:6379 redis:7-alpine"
|
|
233
235
|
```
|
|
234
236
|
|
|
235
|
-
### How
|
|
237
|
+
### How bgrun handles Docker
|
|
236
238
|
|
|
237
|
-
|
|
239
|
+
bgrun is **Docker-aware** — when it detects a `docker run` command, it:
|
|
238
240
|
|
|
239
241
|
1. **Checks container status** via `docker inspect` instead of checking the PID
|
|
240
|
-
2. **Handles container lifecycle** — stops containers with `docker stop` on `
|
|
242
|
+
2. **Handles container lifecycle** — stops containers with `docker stop` on `bgrun --stop`
|
|
241
243
|
3. **Reports correct status** — shows Running/Stopped based on container state, not process state
|
|
242
244
|
|
|
243
245
|
### Docker Compose alternative
|
|
244
246
|
|
|
245
|
-
Instead of `docker-compose.yml`, use
|
|
247
|
+
Instead of `docker-compose.yml`, use bgrun to orchestrate containers alongside your app:
|
|
246
248
|
|
|
247
249
|
```bash
|
|
248
250
|
#!/bin/bash
|
|
249
251
|
# start-stack.sh
|
|
250
252
|
|
|
251
253
|
# Database
|
|
252
|
-
|
|
254
|
+
bgrun --name db \
|
|
253
255
|
--command "docker run --name bgr-db -p 5432:5432 \
|
|
254
256
|
-v pgdata:/var/lib/postgresql/data \
|
|
255
257
|
-e POSTGRES_DB=myapp \
|
|
@@ -258,19 +260,19 @@ bgr --name db \
|
|
|
258
260
|
--force
|
|
259
261
|
|
|
260
262
|
# Cache
|
|
261
|
-
|
|
263
|
+
bgrun --name cache \
|
|
262
264
|
--command "docker run --name bgr-cache -p 6379:6379 redis:7-alpine" \
|
|
263
265
|
--force
|
|
264
266
|
|
|
265
267
|
# Your app (not Docker, just a regular process)
|
|
266
|
-
|
|
268
|
+
bgrun --name api \
|
|
267
269
|
--directory ~/projects/my-api \
|
|
268
270
|
--command "bun run server.ts" \
|
|
269
271
|
--config production.toml \
|
|
270
272
|
--force
|
|
271
273
|
|
|
272
274
|
# See everything
|
|
273
|
-
|
|
275
|
+
bgrun
|
|
274
276
|
```
|
|
275
277
|
|
|
276
278
|
The advantage over Docker Compose: your app processes and Docker containers are managed in the **same place** with the **same commands**.
|
|
@@ -279,16 +281,16 @@ The advantage over Docker Compose: your app processes and Docker containers are
|
|
|
279
281
|
|
|
280
282
|
## Caddy Reverse Proxy
|
|
281
283
|
|
|
282
|
-
|
|
284
|
+
bgrun pairs naturally with [Caddy](https://caddyserver.com/) for production deployments with automatic HTTPS.
|
|
283
285
|
|
|
284
286
|
### Basic setup
|
|
285
287
|
|
|
286
288
|
```bash
|
|
287
|
-
# Start your app on any port (
|
|
288
|
-
|
|
289
|
+
# Start your app on any port (bgrun detects it)
|
|
290
|
+
bgrun --name my-api --command "bun run server.ts" --force
|
|
289
291
|
|
|
290
292
|
# Check which port it got
|
|
291
|
-
|
|
293
|
+
bgrun
|
|
292
294
|
# → my-api ● Running :3000 bun run server.ts
|
|
293
295
|
```
|
|
294
296
|
|
|
@@ -308,55 +310,35 @@ dashboard.example.com {
|
|
|
308
310
|
|
|
309
311
|
```bash
|
|
310
312
|
# Start services
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
bgrun --name api --command "bun run api/server.ts" --force
|
|
314
|
+
bgrun --name frontend --command "bun run frontend/server.ts" --force
|
|
315
|
+
bgrun --name admin --command "bun run admin/server.ts" --force
|
|
314
316
|
|
|
315
317
|
# Start dashboard
|
|
316
|
-
|
|
318
|
+
bgrun --dashboard
|
|
317
319
|
```
|
|
318
320
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
```caddy
|
|
322
|
-
api.example.com {
|
|
323
|
-
reverse_proxy localhost:3000
|
|
324
|
-
}
|
|
321
|
+
### Managing Caddy with bgrun
|
|
325
322
|
|
|
326
|
-
|
|
327
|
-
reverse_proxy localhost:3001
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
admin.example.com {
|
|
331
|
-
reverse_proxy localhost:3002
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
status.example.com {
|
|
335
|
-
reverse_proxy localhost:3003 # BGR dashboard
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Managing Caddy with BGR
|
|
340
|
-
|
|
341
|
-
You can even manage Caddy itself as a BGR process:
|
|
323
|
+
You can even manage Caddy itself as a bgrun process:
|
|
342
324
|
|
|
343
325
|
```bash
|
|
344
|
-
|
|
326
|
+
bgrun --name caddy \
|
|
345
327
|
--directory /etc/caddy \
|
|
346
328
|
--command "caddy run --config Caddyfile" \
|
|
347
329
|
--force
|
|
348
330
|
```
|
|
349
331
|
|
|
350
|
-
Now `
|
|
332
|
+
Now `bgrun` shows your entire stack — app servers, databases, and reverse proxy — in one place.
|
|
351
333
|
|
|
352
334
|
---
|
|
353
335
|
|
|
354
336
|
## TOML Configuration
|
|
355
337
|
|
|
356
|
-
|
|
338
|
+
bgrun loads TOML config files and flattens them into environment variables:
|
|
357
339
|
|
|
358
340
|
```bash
|
|
359
|
-
|
|
341
|
+
bgrun --name api --command "bun run server.ts" --config production.toml
|
|
360
342
|
```
|
|
361
343
|
|
|
362
344
|
```toml
|
|
@@ -386,16 +368,16 @@ AUTH_SESSION_TTL=3600
|
|
|
386
368
|
|
|
387
369
|
The convention: `[section]` becomes the prefix, `key` becomes the suffix, joined with `_`, uppercased.
|
|
388
370
|
|
|
389
|
-
If no `--config` is specified,
|
|
371
|
+
If no `--config` is specified, bgrun looks for `.config.toml` in the working directory automatically.
|
|
390
372
|
|
|
391
373
|
---
|
|
392
374
|
|
|
393
375
|
## Programmatic API
|
|
394
376
|
|
|
395
|
-
|
|
377
|
+
bgrun exposes its internals as importable TypeScript functions:
|
|
396
378
|
|
|
397
379
|
```bash
|
|
398
|
-
bun add
|
|
380
|
+
bun add bgrun
|
|
399
381
|
```
|
|
400
382
|
|
|
401
383
|
### Process management
|
|
@@ -410,7 +392,7 @@ import {
|
|
|
410
392
|
getProcessPorts,
|
|
411
393
|
readFileTail,
|
|
412
394
|
calculateRuntime,
|
|
413
|
-
} from '
|
|
395
|
+
} from 'bgrun'
|
|
414
396
|
|
|
415
397
|
// List all processes
|
|
416
398
|
const procs = getAllProcesses()
|
|
@@ -435,10 +417,10 @@ if (proc) {
|
|
|
435
417
|
}
|
|
436
418
|
|
|
437
419
|
// Read logs
|
|
438
|
-
const
|
|
439
|
-
if (
|
|
440
|
-
const stdout = await readFileTail(
|
|
441
|
-
const stderr = await readFileTail(
|
|
420
|
+
const myProc = getProcess('my-api')
|
|
421
|
+
if (myProc) {
|
|
422
|
+
const stdout = await readFileTail(myProc.stdout_path, 100) // last 100 lines
|
|
423
|
+
const stderr = await readFileTail(myProc.stderr_path, 100)
|
|
442
424
|
}
|
|
443
425
|
|
|
444
426
|
// Stop a process
|
|
@@ -448,7 +430,7 @@ await terminateProcess(proc.pid)
|
|
|
448
430
|
### Build a custom dashboard
|
|
449
431
|
|
|
450
432
|
```typescript
|
|
451
|
-
import { getAllProcesses, isProcessRunning, calculateRuntime } from '
|
|
433
|
+
import { getAllProcesses, isProcessRunning, calculateRuntime } from 'bgrun'
|
|
452
434
|
|
|
453
435
|
// Express/Hono/Elysia endpoint
|
|
454
436
|
export async function GET() {
|
|
@@ -473,20 +455,20 @@ If you're coming from PM2, here's a direct mapping of commands:
|
|
|
473
455
|
|
|
474
456
|
### Command mapping
|
|
475
457
|
|
|
476
|
-
| PM2 |
|
|
477
|
-
|
|
478
|
-
| `pm2 start app.js --name api` | `
|
|
458
|
+
| PM2 | bgrun |
|
|
459
|
+
|-----|-------|
|
|
460
|
+
| `pm2 start app.js --name api` | `bgrun --name api --command "node app.js"` |
|
|
479
461
|
| `pm2 start app.js -i max` | *(cluster mode not supported — use multiple named processes)* |
|
|
480
|
-
| `pm2 list` | `
|
|
481
|
-
| `pm2 show api` | `
|
|
482
|
-
| `pm2 logs api` | `
|
|
483
|
-
| `pm2 logs api --lines 50` | `
|
|
484
|
-
| `pm2 stop api` | `
|
|
485
|
-
| `pm2 restart api` | `
|
|
486
|
-
| `pm2 delete api` | `
|
|
487
|
-
| `pm2 flush` | `
|
|
488
|
-
| `pm2 kill` | `
|
|
489
|
-
| `pm2 monit` | `
|
|
462
|
+
| `pm2 list` | `bgrun` |
|
|
463
|
+
| `pm2 show api` | `bgrun api` |
|
|
464
|
+
| `pm2 logs api` | `bgrun api --logs` |
|
|
465
|
+
| `pm2 logs api --lines 50` | `bgrun api --logs --lines 50` |
|
|
466
|
+
| `pm2 stop api` | `bgrun --stop api` |
|
|
467
|
+
| `pm2 restart api` | `bgrun --restart api` |
|
|
468
|
+
| `pm2 delete api` | `bgrun --delete api` |
|
|
469
|
+
| `pm2 flush` | `bgrun --clean` |
|
|
470
|
+
| `pm2 kill` | `bgrun --nuke` |
|
|
471
|
+
| `pm2 monit` | `bgrun --dashboard` |
|
|
490
472
|
| `pm2 save` / `pm2 resurrect` | *(automatic — processes persist in SQLite)* |
|
|
491
473
|
|
|
492
474
|
### ecosystem.config.js → TOML + shell script
|
|
@@ -513,7 +495,7 @@ module.exports = {
|
|
|
513
495
|
}
|
|
514
496
|
```
|
|
515
497
|
|
|
516
|
-
**
|
|
498
|
+
**bgrun equivalent:**
|
|
517
499
|
|
|
518
500
|
```toml
|
|
519
501
|
# api.toml
|
|
@@ -527,32 +509,32 @@ env = "production"
|
|
|
527
509
|
```bash
|
|
528
510
|
#!/bin/bash
|
|
529
511
|
# start.sh
|
|
530
|
-
|
|
531
|
-
|
|
512
|
+
bgrun --name api --directory ./api --command "node server.js" --config api.toml --force
|
|
513
|
+
bgrun --name worker --directory ./workers --command "node worker.js" --force
|
|
532
514
|
```
|
|
533
515
|
|
|
534
516
|
### Key differences
|
|
535
517
|
|
|
536
|
-
1. **No cluster mode** —
|
|
518
|
+
1. **No cluster mode** — bgrun manages independent processes. For multi-core, run multiple named instances (`api-1`, `api-2`) behind a load balancer.
|
|
537
519
|
|
|
538
|
-
2. **No `pm2 startup`** —
|
|
520
|
+
2. **No `pm2 startup`** — bgrun doesn't install itself as a system service. Use your OS init system (systemd, launchd, Windows Task Scheduler) to run bgrun at boot:
|
|
539
521
|
|
|
540
522
|
```ini
|
|
541
|
-
# /etc/systemd/system/
|
|
523
|
+
# /etc/systemd/system/bgrun-api.service
|
|
542
524
|
[Unit]
|
|
543
|
-
Description=My API via
|
|
525
|
+
Description=My API via bgrun
|
|
544
526
|
|
|
545
527
|
[Service]
|
|
546
|
-
ExecStart=/usr/local/bin/
|
|
528
|
+
ExecStart=/usr/local/bin/bgrun --name api --directory /var/www/api --command "bun run server.ts" --force
|
|
547
529
|
Restart=always
|
|
548
530
|
|
|
549
531
|
[Install]
|
|
550
532
|
WantedBy=multi-user.target
|
|
551
533
|
```
|
|
552
534
|
|
|
553
|
-
3. **No log rotation** —
|
|
535
|
+
3. **No log rotation** — bgrun writes to plain text files in `~/.bgr/`. Use `logrotate` or similar tools, or specify custom log paths with `--stdout` and `--stderr`.
|
|
554
536
|
|
|
555
|
-
4. **Bun required** —
|
|
537
|
+
4. **Bun required** — bgrun runs on Bun, but the *processes it manages* can be anything: Node.js, Python, Ruby, Go, Docker, shell scripts.
|
|
556
538
|
|
|
557
539
|
---
|
|
558
540
|
|
|
@@ -560,10 +542,10 @@ bgr --name worker --directory ./workers --command "node worker.js" --force
|
|
|
560
542
|
|
|
561
543
|
### What happens when a process crashes?
|
|
562
544
|
|
|
563
|
-
|
|
545
|
+
bgrun records the process as **Stopped**. The PID and log files are preserved so you can inspect what happened:
|
|
564
546
|
|
|
565
547
|
```bash
|
|
566
|
-
|
|
548
|
+
bgrun my-api --logs --log-stderr
|
|
567
549
|
```
|
|
568
550
|
|
|
569
551
|
For auto-restart on crash, use the guard script:
|
|
@@ -572,25 +554,25 @@ For auto-restart on crash, use the guard script:
|
|
|
572
554
|
bun run guard.ts my-api 30 # Check every 30 seconds, restart if dead
|
|
573
555
|
```
|
|
574
556
|
|
|
575
|
-
### What happens on `
|
|
557
|
+
### What happens on `bgrun --force` if the port is stuck?
|
|
576
558
|
|
|
577
|
-
|
|
559
|
+
bgrun queries the OS for all TCP ports held by the old PID, kills them, and waits up to 5 seconds for cleanup. If ports are still held after that, the new process starts anyway (and will likely pick a different port).
|
|
578
560
|
|
|
579
561
|
### What happens if I start two processes with the same name?
|
|
580
562
|
|
|
581
|
-
The new process replaces the old one. If the old one is still running, use `--force` to kill it first. Without `--force`,
|
|
563
|
+
The new process replaces the old one. If the old one is still running, use `--force` to kill it first. Without `--force`, bgrun will refuse to start if a process with that name is already running.
|
|
582
564
|
|
|
583
|
-
### What happens if
|
|
565
|
+
### What happens if bgrun itself is killed?
|
|
584
566
|
|
|
585
|
-
The managed processes keep running — they're independent OS processes. When you run `
|
|
567
|
+
The managed processes keep running — they're independent OS processes. When you run `bgrun` again, it reconnects to the SQLite database and checks which PIDs are still alive. Dead processes are marked as **Stopped**.
|
|
586
568
|
|
|
587
569
|
### What about Windows?
|
|
588
570
|
|
|
589
|
-
|
|
571
|
+
bgrun works on Windows. Process management uses `taskkill` and `wmic` instead of Unix signals. Port detection uses `netstat -ano`. The dashboard runs in your browser, so it works everywhere.
|
|
590
572
|
|
|
591
573
|
### Can I manage processes on a remote server?
|
|
592
574
|
|
|
593
|
-
Not directly —
|
|
575
|
+
Not directly — bgrun manages processes on the local machine. For remote management, run bgrun on the remote server and expose the dashboard behind a reverse proxy (see [Caddy section](#caddy-reverse-proxy)).
|
|
594
576
|
|
|
595
577
|
---
|
|
596
578
|
|
|
@@ -599,7 +581,7 @@ Not directly — BGR manages processes on the local machine. For remote manageme
|
|
|
599
581
|
By default, logs go to `~/.bgr/<name>-out.txt` and `~/.bgr/<name>-err.txt`. Override with:
|
|
600
582
|
|
|
601
583
|
```bash
|
|
602
|
-
|
|
584
|
+
bgrun --name api \
|
|
603
585
|
--command "bun run server.ts" \
|
|
604
586
|
--stdout /var/log/api/stdout.log \
|
|
605
587
|
--stderr /var/log/api/stderr.log
|
|
@@ -612,15 +594,15 @@ bgr --name api \
|
|
|
612
594
|
Tag processes with `BGR_GROUP` to organize and filter them:
|
|
613
595
|
|
|
614
596
|
```bash
|
|
615
|
-
BGR_GROUP=prod
|
|
616
|
-
BGR_GROUP=prod
|
|
617
|
-
BGR_GROUP=dev
|
|
597
|
+
BGR_GROUP=prod bgrun --name api --command "bun run server.ts" --force
|
|
598
|
+
BGR_GROUP=prod bgrun --name worker --command "bun run worker.ts" --force
|
|
599
|
+
BGR_GROUP=dev bgrun --name dev-server --command "bun run dev" --force
|
|
618
600
|
|
|
619
601
|
# Show only production processes
|
|
620
|
-
|
|
602
|
+
bgrun --filter prod
|
|
621
603
|
|
|
622
604
|
# Show only dev processes
|
|
623
|
-
|
|
605
|
+
bgrun --filter dev
|
|
624
606
|
```
|
|
625
607
|
|
|
626
608
|
---
|
|
@@ -630,7 +612,7 @@ bgr --filter dev
|
|
|
630
612
|
Pull the latest changes before starting:
|
|
631
613
|
|
|
632
614
|
```bash
|
|
633
|
-
|
|
615
|
+
bgrun --name api \
|
|
634
616
|
--directory ~/projects/api \
|
|
635
617
|
--command "bun run server.ts" \
|
|
636
618
|
--fetch \
|
|
@@ -641,7 +623,7 @@ bgr --name api \
|
|
|
641
623
|
|
|
642
624
|
```bash
|
|
643
625
|
# Deploy script
|
|
644
|
-
|
|
626
|
+
bgrun --name api --directory /var/www/api --command "bun run server.ts" --fetch --force
|
|
645
627
|
```
|
|
646
628
|
|
|
647
629
|
---
|
|
@@ -687,6 +669,7 @@ All state lives in `~/.bgr/`. To reset everything, delete this directory.
|
|
|
687
669
|
| `--clean` | Remove stopped processes | - |
|
|
688
670
|
| `--nuke` | Delete ALL processes | - |
|
|
689
671
|
| `--dashboard` | Launch web dashboard | - |
|
|
672
|
+
| `--port <number>` | Port for dashboard | 3000 |
|
|
690
673
|
| `--version` | Show version | - |
|
|
691
674
|
| `--help` | Show help | - |
|
|
692
675
|
|
|
@@ -717,4 +700,3 @@ MIT
|
|
|
717
700
|
Built by [Mements](https://github.com/Mements) with ⚡ Bun
|
|
718
701
|
|
|
719
702
|
</div>
|
|
720
|
-
]]>
|
package/dist/index.js
CHANGED
|
@@ -1,37 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __export = (target, all) => {
|
|
5
|
-
for (var name in all)
|
|
6
|
-
__defProp(target, name, {
|
|
7
|
-
get: all[name],
|
|
8
|
-
enumerable: true,
|
|
9
|
-
configurable: true,
|
|
10
|
-
set: (newValue) => all[name] = () => newValue
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
3
|
var __require = import.meta.require;
|
|
15
4
|
|
|
5
|
+
// src/index.ts
|
|
6
|
+
import { parseArgs } from "util";
|
|
7
|
+
|
|
16
8
|
// src/platform.ts
|
|
17
|
-
var exports_platform = {};
|
|
18
|
-
__export(exports_platform, {
|
|
19
|
-
waitForPortFree: () => waitForPortFree,
|
|
20
|
-
terminateProcess: () => terminateProcess,
|
|
21
|
-
readFileTail: () => readFileTail,
|
|
22
|
-
killProcessOnPort: () => killProcessOnPort,
|
|
23
|
-
isWindows: () => isWindows,
|
|
24
|
-
isProcessRunning: () => isProcessRunning,
|
|
25
|
-
isPortFree: () => isPortFree,
|
|
26
|
-
getShellCommand: () => getShellCommand,
|
|
27
|
-
getProcessPorts: () => getProcessPorts,
|
|
28
|
-
getProcessMemory: () => getProcessMemory,
|
|
29
|
-
getHomeDir: () => getHomeDir,
|
|
30
|
-
findPidByPort: () => findPidByPort,
|
|
31
|
-
findChildPid: () => findChildPid,
|
|
32
|
-
ensureDir: () => ensureDir,
|
|
33
|
-
copyFile: () => copyFile
|
|
34
|
-
});
|
|
35
9
|
import * as fs from "fs";
|
|
36
10
|
import * as os from "os";
|
|
37
11
|
var {$ } = globalThis.Bun;
|
|
@@ -225,44 +199,6 @@ async function findChildPid(parentPid) {
|
|
|
225
199
|
}
|
|
226
200
|
return currentPid;
|
|
227
201
|
}
|
|
228
|
-
async function findPidByPort(port, maxWaitMs = 8000) {
|
|
229
|
-
const start = Date.now();
|
|
230
|
-
const pollMs = 500;
|
|
231
|
-
while (Date.now() - start < maxWaitMs) {
|
|
232
|
-
try {
|
|
233
|
-
if (isWindows()) {
|
|
234
|
-
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
235
|
-
for (const line of result.split(`
|
|
236
|
-
`)) {
|
|
237
|
-
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
238
|
-
const parts = line.trim().split(/\s+/);
|
|
239
|
-
const pid = parseInt(parts[parts.length - 1]);
|
|
240
|
-
if (!isNaN(pid) && pid > 0)
|
|
241
|
-
return pid;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else {
|
|
245
|
-
try {
|
|
246
|
-
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
247
|
-
for (const line of result2.split(`
|
|
248
|
-
`)) {
|
|
249
|
-
if (line.includes(`:${port}`)) {
|
|
250
|
-
const pidMatch = line.match(/pid=(\d+)/);
|
|
251
|
-
if (pidMatch)
|
|
252
|
-
return parseInt(pidMatch[1]);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
} catch {}
|
|
256
|
-
const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
|
|
257
|
-
const pid = parseInt(result.trim());
|
|
258
|
-
if (!isNaN(pid) && pid > 0)
|
|
259
|
-
return pid;
|
|
260
|
-
}
|
|
261
|
-
} catch {}
|
|
262
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
263
|
-
}
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
202
|
async function readFileTail(filePath, lines) {
|
|
267
203
|
try {
|
|
268
204
|
const content = await Bun.file(filePath).text();
|
|
@@ -277,28 +213,6 @@ async function readFileTail(filePath, lines) {
|
|
|
277
213
|
throw new Error(`Error reading file: ${error}`);
|
|
278
214
|
}
|
|
279
215
|
}
|
|
280
|
-
function copyFile(src, dest) {
|
|
281
|
-
fs.copyFileSync(src, dest);
|
|
282
|
-
}
|
|
283
|
-
async function getProcessMemory(pid) {
|
|
284
|
-
try {
|
|
285
|
-
if (isWindows()) {
|
|
286
|
-
const result = await $`wmic process where ProcessId=${pid} get WorkingSetSize`.nothrow().text();
|
|
287
|
-
const lines = result.split(`
|
|
288
|
-
`).filter((line) => line.trim() && !line.includes("WorkingSetSize"));
|
|
289
|
-
if (lines.length > 0) {
|
|
290
|
-
return parseInt(lines[0].trim()) || 0;
|
|
291
|
-
}
|
|
292
|
-
return 0;
|
|
293
|
-
} else {
|
|
294
|
-
const result = await $`ps -o rss= -p ${pid}`.text();
|
|
295
|
-
const memoryKB = parseInt(result.trim());
|
|
296
|
-
return memoryKB * 1024;
|
|
297
|
-
}
|
|
298
|
-
} catch {
|
|
299
|
-
return 0;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
216
|
async function getProcessPorts(pid) {
|
|
303
217
|
try {
|
|
304
218
|
if (isWindows()) {
|
|
@@ -345,15 +259,9 @@ async function getProcessPorts(pid) {
|
|
|
345
259
|
return [];
|
|
346
260
|
}
|
|
347
261
|
}
|
|
348
|
-
var init_platform = () => {};
|
|
349
|
-
|
|
350
|
-
// src/index.ts
|
|
351
|
-
import { parseArgs } from "util";
|
|
352
262
|
|
|
353
263
|
// src/utils.ts
|
|
354
|
-
init_platform();
|
|
355
264
|
import * as fs2 from "fs";
|
|
356
|
-
import { join } from "path";
|
|
357
265
|
import chalk from "chalk";
|
|
358
266
|
function parseEnvString(envString) {
|
|
359
267
|
const env = {};
|
|
@@ -372,6 +280,7 @@ function calculateRuntime(startTime) {
|
|
|
372
280
|
}
|
|
373
281
|
async function getVersion() {
|
|
374
282
|
try {
|
|
283
|
+
const { join } = await import("path");
|
|
375
284
|
const pkgPath = join(import.meta.dir, "../package.json");
|
|
376
285
|
const pkg = await Bun.file(pkgPath).json();
|
|
377
286
|
return pkg.version || "0.0.0";
|
|
@@ -430,9 +339,8 @@ function tailFile(path, prefix, colorFn, lines) {
|
|
|
430
339
|
}
|
|
431
340
|
|
|
432
341
|
// src/db.ts
|
|
433
|
-
init_platform();
|
|
434
342
|
import { Database, z } from "sqlite-zod-orm";
|
|
435
|
-
import { join
|
|
343
|
+
import { join } from "path";
|
|
436
344
|
var {sleep } = globalThis.Bun;
|
|
437
345
|
var ProcessSchema = z.object({
|
|
438
346
|
pid: z.number(),
|
|
@@ -447,8 +355,8 @@ var ProcessSchema = z.object({
|
|
|
447
355
|
});
|
|
448
356
|
var homePath = getHomeDir();
|
|
449
357
|
var dbName = process.env.DB_NAME ?? "bgr";
|
|
450
|
-
var dbPath =
|
|
451
|
-
ensureDir(
|
|
358
|
+
var dbPath = join(homePath, ".bgr", `${dbName}_v2.sqlite`);
|
|
359
|
+
ensureDir(join(homePath, ".bgr"));
|
|
452
360
|
var db = new Database(dbPath, {
|
|
453
361
|
process: ProcessSchema
|
|
454
362
|
}, {
|
|
@@ -501,9 +409,6 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
|
501
409
|
throw new Error("Max retries reached for database operation");
|
|
502
410
|
}
|
|
503
411
|
|
|
504
|
-
// src/commands/run.ts
|
|
505
|
-
init_platform();
|
|
506
|
-
|
|
507
412
|
// src/logger.ts
|
|
508
413
|
import boxen from "boxen";
|
|
509
414
|
import chalk2 from "chalk";
|
|
@@ -563,7 +468,7 @@ async function parseConfigFile(configPath) {
|
|
|
563
468
|
// src/commands/run.ts
|
|
564
469
|
var {$: $2 } = globalThis.Bun;
|
|
565
470
|
var {sleep: sleep2 } = globalThis.Bun;
|
|
566
|
-
import { join as
|
|
471
|
+
import { join as join2 } from "path";
|
|
567
472
|
var homePath2 = getHomeDir();
|
|
568
473
|
async function handleRun(options) {
|
|
569
474
|
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
@@ -630,7 +535,7 @@ async function handleRun(options) {
|
|
|
630
535
|
finalConfigPath = ".config.toml";
|
|
631
536
|
}
|
|
632
537
|
if (finalConfigPath) {
|
|
633
|
-
const fullConfigPath =
|
|
538
|
+
const fullConfigPath = join2(finalDirectory, finalConfigPath);
|
|
634
539
|
if (await Bun.file(fullConfigPath).exists()) {
|
|
635
540
|
try {
|
|
636
541
|
const newConfigEnv = await parseConfigFile(fullConfigPath);
|
|
@@ -643,9 +548,9 @@ async function handleRun(options) {
|
|
|
643
548
|
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
644
549
|
}
|
|
645
550
|
}
|
|
646
|
-
const stdoutPath = stdout || existingProcess?.stdout_path ||
|
|
551
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join2(homePath2, ".bgr", `${name}-out.txt`);
|
|
647
552
|
Bun.write(stdoutPath, "");
|
|
648
|
-
const stderrPath = stderr || existingProcess?.stderr_path ||
|
|
553
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join2(homePath2, ".bgr", `${name}-err.txt`);
|
|
649
554
|
Bun.write(stderrPath, "");
|
|
650
555
|
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
651
556
|
env: { ...Bun.env, ...finalEnv },
|
|
@@ -759,9 +664,11 @@ function renderHorizontalTable(rows, columns, options = {}) {
|
|
|
759
664
|
return { table: chalk3.gray("No data to display"), truncatedIndices: [] };
|
|
760
665
|
const borderChars = {
|
|
761
666
|
rounded: ["\u256D", "\u252C", "\u256E", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2570", "\u2534", "\u256F"],
|
|
667
|
+
single: ["\u250C", "\u252C", "\u2510", "\u2500", "\u2502", "\u251C", "\u253C", "\u2524", "\u2514", "\u2534", "\u2518"],
|
|
668
|
+
double: ["\u2554", "\u2566", "\u2557", "\u2550", "\u2551", "\u2560", "\u256C", "\u2563", "\u255A", "\u2569", "\u255D"],
|
|
762
669
|
none: [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "]
|
|
763
|
-
}
|
|
764
|
-
const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars;
|
|
670
|
+
};
|
|
671
|
+
const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars[borderStyle] ?? borderChars.rounded;
|
|
765
672
|
const columnWidths = calculateColumnWidths(rows, columns, maxWidth, padding);
|
|
766
673
|
const widthArray = columns.map((col) => columnWidths.get(col.key));
|
|
767
674
|
const truncatedIndices = new Set;
|
|
@@ -834,7 +741,6 @@ function renderProcessTable(processes, options) {
|
|
|
834
741
|
}
|
|
835
742
|
|
|
836
743
|
// src/commands/list.ts
|
|
837
|
-
init_platform();
|
|
838
744
|
async function showAll(opts) {
|
|
839
745
|
const processes = getAllProcesses();
|
|
840
746
|
const filtered = processes.filter((proc) => {
|
|
@@ -896,7 +802,6 @@ async function showAll(opts) {
|
|
|
896
802
|
}
|
|
897
803
|
|
|
898
804
|
// src/commands/cleanup.ts
|
|
899
|
-
init_platform();
|
|
900
805
|
import * as fs3 from "fs";
|
|
901
806
|
async function handleDelete(name) {
|
|
902
807
|
const process2 = getProcess(name);
|
|
@@ -961,11 +866,10 @@ async function handleStop(name) {
|
|
|
961
866
|
announce(`Process '${name}' is already stopped.`, "Process Stop");
|
|
962
867
|
return;
|
|
963
868
|
}
|
|
964
|
-
const
|
|
965
|
-
const ports = await getProcessPorts2(proc.pid);
|
|
869
|
+
const ports = await getProcessPorts(proc.pid);
|
|
966
870
|
await terminateProcess(proc.pid);
|
|
967
871
|
for (const port of ports) {
|
|
968
|
-
await
|
|
872
|
+
await killProcessOnPort(port);
|
|
969
873
|
}
|
|
970
874
|
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
971
875
|
}
|
|
@@ -975,21 +879,20 @@ async function handleDeleteAll() {
|
|
|
975
879
|
announce("There are no processes to delete.", "Delete All");
|
|
976
880
|
return;
|
|
977
881
|
}
|
|
978
|
-
const { getProcessPorts: getProcessPorts2, killProcessOnPort: killProcessOnPort2, waitForPortFree: waitForPortFree2 } = await Promise.resolve().then(() => (init_platform(), exports_platform));
|
|
979
882
|
let killedCount = 0;
|
|
980
883
|
let portsFreed = 0;
|
|
981
884
|
for (const proc of processes) {
|
|
982
885
|
const running = await isProcessRunning(proc.pid);
|
|
983
886
|
if (running) {
|
|
984
|
-
const ports = await
|
|
887
|
+
const ports = await getProcessPorts(proc.pid);
|
|
985
888
|
await terminateProcess(proc.pid, true);
|
|
986
889
|
killedCount++;
|
|
987
890
|
for (const port of ports) {
|
|
988
|
-
await
|
|
989
|
-
const freed = await
|
|
891
|
+
await killProcessOnPort(port);
|
|
892
|
+
const freed = await waitForPortFree(port, 3000);
|
|
990
893
|
if (!freed) {
|
|
991
|
-
await
|
|
992
|
-
await
|
|
894
|
+
await killProcessOnPort(port);
|
|
895
|
+
await waitForPortFree(port, 2000);
|
|
993
896
|
}
|
|
994
897
|
portsFreed++;
|
|
995
898
|
}
|
|
@@ -1015,9 +918,7 @@ async function handleDeleteAll() {
|
|
|
1015
918
|
}
|
|
1016
919
|
|
|
1017
920
|
// src/commands/watch.ts
|
|
1018
|
-
init_platform();
|
|
1019
921
|
import * as fs4 from "fs";
|
|
1020
|
-
import { join as join4 } from "path";
|
|
1021
922
|
import path from "path";
|
|
1022
923
|
import chalk5 from "chalk";
|
|
1023
924
|
async function handleWatch(options, logOptions) {
|
|
@@ -1177,7 +1078,7 @@ ${color(content)}
|
|
|
1177
1078
|
const watcher = fs4.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
1178
1079
|
if (filename == null)
|
|
1179
1080
|
return;
|
|
1180
|
-
const fullPath =
|
|
1081
|
+
const fullPath = path.join(workdir, filename);
|
|
1181
1082
|
if (fullPath.includes(".git") || fullPath.includes("node_modules"))
|
|
1182
1083
|
return;
|
|
1183
1084
|
if (debounceTimeout)
|
|
@@ -1205,7 +1106,6 @@ SIGINT received...`));
|
|
|
1205
1106
|
}
|
|
1206
1107
|
|
|
1207
1108
|
// src/commands/logs.ts
|
|
1208
|
-
init_platform();
|
|
1209
1109
|
import chalk6 from "chalk";
|
|
1210
1110
|
import * as fs5 from "fs";
|
|
1211
1111
|
async function showLogs(name, logType = "both", lines) {
|
|
@@ -1249,7 +1149,6 @@ async function showLogs(name, logType = "both", lines) {
|
|
|
1249
1149
|
}
|
|
1250
1150
|
|
|
1251
1151
|
// src/commands/details.ts
|
|
1252
|
-
init_platform();
|
|
1253
1152
|
import chalk7 from "chalk";
|
|
1254
1153
|
async function showDetails(name) {
|
|
1255
1154
|
const proc = getProcess(name);
|
|
@@ -1289,20 +1188,19 @@ import { start } from "melina";
|
|
|
1289
1188
|
import path2 from "path";
|
|
1290
1189
|
async function startServer() {
|
|
1291
1190
|
const appDir = path2.join(import.meta.dir, "../dashboard/app");
|
|
1292
|
-
const
|
|
1191
|
+
const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
|
|
1293
1192
|
await start({
|
|
1294
1193
|
appDir,
|
|
1295
1194
|
defaultTitle: "bgrun Dashboard - Process Manager",
|
|
1296
1195
|
globalCss: path2.join(appDir, "globals.css"),
|
|
1297
|
-
port
|
|
1196
|
+
...explicitPort !== undefined && { port: explicitPort }
|
|
1298
1197
|
});
|
|
1299
1198
|
}
|
|
1300
1199
|
|
|
1301
1200
|
// src/index.ts
|
|
1302
|
-
init_platform();
|
|
1303
1201
|
import dedent from "dedent";
|
|
1304
1202
|
import chalk8 from "chalk";
|
|
1305
|
-
import { join as
|
|
1203
|
+
import { join as join3 } from "path";
|
|
1306
1204
|
var {sleep: sleep3 } = globalThis.Bun;
|
|
1307
1205
|
async function showHelp() {
|
|
1308
1206
|
const usage = dedent`
|
|
@@ -1389,7 +1287,7 @@ async function run() {
|
|
|
1389
1287
|
if (values.dashboard) {
|
|
1390
1288
|
const dashboardName = "bgr-dashboard";
|
|
1391
1289
|
const homePath3 = getHomeDir();
|
|
1392
|
-
const bgrDir =
|
|
1290
|
+
const bgrDir = join3(homePath3, ".bgr");
|
|
1393
1291
|
const requestedPort = values.port;
|
|
1394
1292
|
const existing = getProcess(dashboardName);
|
|
1395
1293
|
if (existing && await isProcessRunning(existing.pid)) {
|
|
@@ -1418,8 +1316,8 @@ async function run() {
|
|
|
1418
1316
|
const scriptPath = resolve(process.argv[1]);
|
|
1419
1317
|
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
1420
1318
|
const command = `bgrun --_serve`;
|
|
1421
|
-
const stdoutPath =
|
|
1422
|
-
const stderrPath =
|
|
1319
|
+
const stdoutPath = join3(bgrDir, `${dashboardName}-out.txt`);
|
|
1320
|
+
const stderrPath = join3(bgrDir, `${dashboardName}-err.txt`);
|
|
1423
1321
|
await Bun.write(stdoutPath, "");
|
|
1424
1322
|
await Bun.write(stderrPath, "");
|
|
1425
1323
|
const spawnEnv = { ...Bun.env };
|
package/package.json
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "bgrun",
|
|
3
|
-
"version": "3.3.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"engines": {
|
|
58
|
-
"bun": ">=1.0.0"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "bgrun",
|
|
3
|
+
"version": "3.3.3",
|
|
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
|
+
}
|
package/src/api.ts
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
// --- Types ---
|
|
22
|
+
export type { Process } from './db'
|
|
23
|
+
export type { CommandOptions } from './types'
|
|
24
|
+
|
|
21
25
|
// --- Database Operations ---
|
|
22
26
|
export { db, getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation } from './db'
|
|
23
27
|
|
package/src/commands/cleanup.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
import { getProcess, removeProcessByName, removeProcess, getAllProcesses, removeAllProcesses } from "../db";
|
|
3
|
-
import { isProcessRunning, terminateProcess } from "../platform";
|
|
3
|
+
import { isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree } from "../platform";
|
|
4
4
|
import { announce, error } from "../logger";
|
|
5
|
-
import type { ProcessRecord } from "../types";
|
|
6
5
|
import * as fs from "fs";
|
|
7
6
|
|
|
8
7
|
export async function handleDelete(name: string) {
|
|
9
|
-
const process = getProcess(name);
|
|
10
|
-
// getProcess returns ProcessRecord | null
|
|
8
|
+
const process = getProcess(name);
|
|
11
9
|
|
|
12
10
|
if (!process) {
|
|
13
11
|
error(`No process found named '${name}'`);
|
|
14
|
-
// error calls process.exit(1), so we return but TS doesn't know.
|
|
15
12
|
return;
|
|
16
13
|
}
|
|
17
14
|
|
|
@@ -76,7 +73,6 @@ export async function handleStop(name: string) {
|
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
// Detect ports the process is using BEFORE killing it
|
|
79
|
-
const { getProcessPorts, killProcessOnPort } = await import("../platform");
|
|
80
76
|
const ports = await getProcessPorts(proc.pid);
|
|
81
77
|
|
|
82
78
|
await terminateProcess(proc.pid);
|
|
@@ -96,7 +92,6 @@ export async function handleDeleteAll() {
|
|
|
96
92
|
return;
|
|
97
93
|
}
|
|
98
94
|
|
|
99
|
-
const { getProcessPorts, killProcessOnPort, waitForPortFree } = await import("../platform");
|
|
100
95
|
let killedCount = 0;
|
|
101
96
|
let portsFreed = 0;
|
|
102
97
|
|
package/src/commands/list.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { renderProcessTable } from "../table";
|
|
3
|
+
import type { ProcessTableRow } from "../table";
|
|
4
4
|
import { getAllProcesses } from "../db";
|
|
5
5
|
import { announce } from "../logger";
|
|
6
6
|
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
package/src/commands/watch.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import type { CommandOptions
|
|
1
|
+
import type { CommandOptions } from "../types";
|
|
2
|
+
import type { Process } from "../db";
|
|
2
3
|
import { getProcess } from "../db";
|
|
3
4
|
import { isProcessRunning } from "../platform";
|
|
4
5
|
import { error, announce } from "../logger";
|
|
5
6
|
import { tailFile } from "../utils";
|
|
6
7
|
import { handleRun } from "./run";
|
|
7
8
|
import * as fs from "fs";
|
|
8
|
-
import { join } from "path";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import chalk, { type ChalkInstance } from "chalk";
|
|
11
11
|
|
|
12
12
|
export async function handleWatch(options: CommandOptions, logOptions: { showLogs: boolean; logType: 'stdout' | 'stderr' | 'both', lines?: number }) {
|
|
13
|
-
let currentProcess:
|
|
13
|
+
let currentProcess: Process | null = null;
|
|
14
14
|
let isRestarting = false;
|
|
15
15
|
let debounceTimeout: Timer | null = null;
|
|
16
16
|
let tailStops: (() => void)[] = [];
|
|
17
17
|
let lastRestartPath: string | null = null; // Track if restart was due to file change
|
|
18
18
|
|
|
19
|
-
const dumpLogsIfDead = async (proc:
|
|
19
|
+
const dumpLogsIfDead = async (proc: Process, reason: string) => {
|
|
20
20
|
const isDead = !(await isProcessRunning(proc.pid));
|
|
21
21
|
if (!isDead) return false;
|
|
22
22
|
|
|
@@ -193,7 +193,7 @@ export async function handleWatch(options: CommandOptions, logOptions: { showLog
|
|
|
193
193
|
|
|
194
194
|
const watcher = fs.watch(workdir, { recursive: true }, (eventType, filename) => {
|
|
195
195
|
if (filename == null) return;
|
|
196
|
-
const fullPath = join(workdir, filename as string);
|
|
196
|
+
const fullPath = path.join(workdir, filename as string);
|
|
197
197
|
if (fullPath.includes(".git") || fullPath.includes("node_modules")) return;
|
|
198
198
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
|
199
199
|
debounceTimeout = setTimeout(() => restartProcess(fullPath), 500);
|
package/src/config.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -14,11 +14,13 @@ import path from 'path';
|
|
|
14
14
|
export async function startServer() {
|
|
15
15
|
const appDir = path.join(import.meta.dir, '../dashboard/app');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Only pass port when BUN_PORT is explicitly set.
|
|
18
|
+
// When omitted, Melina defaults to 3000 with auto-fallback to next available port.
|
|
19
|
+
const explicitPort = process.env.BUN_PORT ? parseInt(process.env.BUN_PORT, 10) : undefined;
|
|
18
20
|
await start({
|
|
19
21
|
appDir,
|
|
20
22
|
defaultTitle: 'bgrun Dashboard - Process Manager',
|
|
21
23
|
globalCss: path.join(appDir, 'globals.css'),
|
|
22
|
-
port,
|
|
24
|
+
...(explicitPort !== undefined && { port: explicitPort }),
|
|
23
25
|
});
|
|
24
26
|
}
|
package/src/table.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
// ./Documents/bgr/src/table.ts
|
|
4
|
-
|
|
5
3
|
import chalk from "chalk";
|
|
6
4
|
|
|
7
5
|
export interface TableColumn {
|
|
@@ -14,7 +12,7 @@ export interface TableColumn {
|
|
|
14
12
|
export interface TableOptions {
|
|
15
13
|
maxWidth?: number;
|
|
16
14
|
padding?: number;
|
|
17
|
-
borderStyle?: "
|
|
15
|
+
borderStyle?: "rounded" | "single" | "double" | "none";
|
|
18
16
|
showHeaders?: boolean;
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -145,11 +143,13 @@ export function renderHorizontalTable(
|
|
|
145
143
|
const { maxWidth = getTerminalWidth(), padding = 2, borderStyle = "rounded", showHeaders = true } = options;
|
|
146
144
|
if (rows.length === 0) return { table: chalk.gray("No data to display"), truncatedIndices: [] };
|
|
147
145
|
|
|
148
|
-
const borderChars = {
|
|
146
|
+
const borderChars: Record<string, string[]> = {
|
|
149
147
|
rounded: ["╭", "┬", "╮", "─", "│", "├", "┼", "┤", "╰", "┴", "╯"],
|
|
148
|
+
single: ["┌", "┬", "┐", "─", "│", "├", "┼", "┤", "└", "┴", "┘"],
|
|
149
|
+
double: ["╔", "╦", "╗", "═", "║", "╠", "╬", "╣", "╚", "╩", "╝"],
|
|
150
150
|
none: [" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
|
151
|
-
}
|
|
152
|
-
const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars;
|
|
151
|
+
};
|
|
152
|
+
const [tl, tc, tr, h, v, ml, mc, mr, bl, bc, br] = borderChars[borderStyle] ?? borderChars.rounded;
|
|
153
153
|
const columnWidths = calculateColumnWidths(rows, columns, maxWidth, padding);
|
|
154
154
|
const widthArray = columns.map((col) => columnWidths.get(col.key)!);
|
|
155
155
|
const truncatedIndices = new Set<number>();
|
package/src/types.ts
CHANGED
|
@@ -12,16 +12,3 @@ export interface CommandOptions {
|
|
|
12
12
|
stderr?: string;
|
|
13
13
|
dbPath?: string;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
export interface ProcessRecord {
|
|
17
|
-
id: number;
|
|
18
|
-
pid: number;
|
|
19
|
-
workdir: string;
|
|
20
|
-
command: string;
|
|
21
|
-
name: string;
|
|
22
|
-
env: string;
|
|
23
|
-
timestamp: string;
|
|
24
|
-
configPath?: string;
|
|
25
|
-
stdout_path: string;
|
|
26
|
-
stderr_path: string;
|
|
27
|
-
}
|
package/src/utils.ts
CHANGED
|
@@ -16,16 +16,16 @@ export function calculateRuntime(startTime: string): string {
|
|
|
16
16
|
return `${diffInMinutes} minutes`;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
// Re-export
|
|
19
|
+
// Re-export platform utils for backward compatibility and convenience
|
|
20
20
|
export { isProcessRunning } from "./platform";
|
|
21
21
|
|
|
22
22
|
import * as fs from "fs";
|
|
23
|
-
import { join } from "path";
|
|
24
23
|
import chalk from "chalk";
|
|
25
24
|
|
|
26
25
|
// Read version at runtime instead of using macros (macros crash on Windows)
|
|
27
26
|
export async function getVersion(): Promise<string> {
|
|
28
27
|
try {
|
|
28
|
+
const { join } = await import("path");
|
|
29
29
|
const pkgPath = join(import.meta.dir, '../package.json');
|
|
30
30
|
const pkg = await Bun.file(pkgPath).json();
|
|
31
31
|
return pkg.version || '0.0.0';
|
|
@@ -45,8 +45,6 @@ export function validateDirectory(directory: string) {
|
|
|
45
45
|
export function tailFile(path: string, prefix: string, colorFn: (s: string) => string, lines?: number): () => void {
|
|
46
46
|
let position = 0;
|
|
47
47
|
let lastPartial = '';
|
|
48
|
-
// Check if file exists first? The original code did fs.openSync which throws if not exist.
|
|
49
|
-
// Assuming caller checks persistence or we catch.
|
|
50
48
|
|
|
51
49
|
if (!fs.existsSync(path)) {
|
|
52
50
|
return () => { };
|
|
@@ -96,4 +94,3 @@ export function tailFile(path: string, prefix: string, colorFn: (s: string) => s
|
|
|
96
94
|
try { fs.closeSync(fd); } catch { }
|
|
97
95
|
};
|
|
98
96
|
}
|
|
99
|
-
|
package/src/schema.ts
DELETED
package/src/version.macro.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { file } from 'bun';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Reads the project's version from package.json at build time.
|
|
6
|
-
* The returned value is directly inlined into the bundle.
|
|
7
|
-
*/
|
|
8
|
-
export async function getVersion(): Promise<string> {
|
|
9
|
-
try {
|
|
10
|
-
const pkgPath = join(import.meta.dir, '../package.json');
|
|
11
|
-
const pkg = await file(pkgPath).json();
|
|
12
|
-
return pkg.version || '0.0.0';
|
|
13
|
-
} catch (err) {
|
|
14
|
-
console.error("Failed to read version from package.json during build:", err);
|
|
15
|
-
return '0.0.0-error';
|
|
16
|
-
}
|
|
17
|
-
}
|