bgrun 3.12.2 → 3.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +17 -13
- package/package.json +19 -4
- package/src/platform.ts +15 -8
- package/src/bgrun.test.ts +0 -280
- package/src/index_copy.ts +0 -614
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(exports_platform, {
|
|
|
25
25
|
reconcileProcessPids: () => reconcileProcessPids,
|
|
26
26
|
readFileTail: () => readFileTail,
|
|
27
27
|
psExec: () => psExec,
|
|
28
|
+
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
28
29
|
killProcessOnPort: () => killProcessOnPort,
|
|
29
30
|
isWindows: () => isWindows,
|
|
30
31
|
isProcessRunning: () => isProcessRunning,
|
|
@@ -457,6 +458,17 @@ async function getProcessBatchResources(pids) {
|
|
|
457
458
|
return resourceMap;
|
|
458
459
|
}) ?? new Map;
|
|
459
460
|
}
|
|
461
|
+
function parseUnixListeningPorts(output) {
|
|
462
|
+
const ports = new Set;
|
|
463
|
+
for (const line of output.split(`
|
|
464
|
+
`)) {
|
|
465
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
466
|
+
if (portMatch) {
|
|
467
|
+
ports.add(parseInt(portMatch[1]));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return Array.from(ports);
|
|
471
|
+
}
|
|
460
472
|
async function getProcessPorts(pid) {
|
|
461
473
|
try {
|
|
462
474
|
if (isWindows()) {
|
|
@@ -473,29 +485,21 @@ async function getProcessPorts(pid) {
|
|
|
473
485
|
} else {
|
|
474
486
|
try {
|
|
475
487
|
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
476
|
-
const
|
|
488
|
+
const ports = new Set;
|
|
477
489
|
for (const line of result2.split(`
|
|
478
490
|
`)) {
|
|
479
491
|
if (line.includes(`pid=${pid}`)) {
|
|
480
492
|
const portMatch = line.match(/:(\d+)\s/);
|
|
481
493
|
if (portMatch) {
|
|
482
|
-
|
|
494
|
+
ports.add(parseInt(portMatch[1]));
|
|
483
495
|
}
|
|
484
496
|
}
|
|
485
497
|
}
|
|
486
|
-
if (
|
|
487
|
-
return Array.from(
|
|
498
|
+
if (ports.size > 0)
|
|
499
|
+
return Array.from(ports);
|
|
488
500
|
} catch {}
|
|
489
501
|
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
490
|
-
|
|
491
|
-
for (const line of result.split(`
|
|
492
|
-
`)) {
|
|
493
|
-
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
494
|
-
if (portMatch) {
|
|
495
|
-
ports.add(parseInt(portMatch[1]));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return Array.from(ports);
|
|
502
|
+
return parseUnixListeningPorts(result);
|
|
499
503
|
}
|
|
500
504
|
} catch {
|
|
501
505
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bgrun",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.4",
|
|
4
4
|
"description": "bgrun — A lightweight process manager for Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/api.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
".": "./src/api.ts"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"bgrun": "
|
|
11
|
+
"bgrun": "dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "bun run ./src/build.ts",
|
|
@@ -17,7 +17,22 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"src",
|
|
20
|
+
"src/api.ts",
|
|
21
|
+
"src/build.ts",
|
|
22
|
+
"src/config.ts",
|
|
23
|
+
"src/db.ts",
|
|
24
|
+
"src/deploy.ts",
|
|
25
|
+
"src/deps.ts",
|
|
26
|
+
"src/guard.ts",
|
|
27
|
+
"src/index.ts",
|
|
28
|
+
"src/log-rotation.ts",
|
|
29
|
+
"src/logger.ts",
|
|
30
|
+
"src/platform.ts",
|
|
31
|
+
"src/server.ts",
|
|
32
|
+
"src/table.ts",
|
|
33
|
+
"src/types.ts",
|
|
34
|
+
"src/utils.ts",
|
|
35
|
+
"src/commands",
|
|
21
36
|
"dashboard/app",
|
|
22
37
|
"scripts",
|
|
23
38
|
"README.md",
|
|
@@ -37,7 +52,7 @@
|
|
|
37
52
|
"license": "MIT",
|
|
38
53
|
"repository": {
|
|
39
54
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/7flash/bgrun.git"
|
|
55
|
+
"url": "git+https://github.com/7flash/bgrun.git"
|
|
41
56
|
},
|
|
42
57
|
"devDependencies": {
|
|
43
58
|
"@types/bun": "^1.3.10",
|
package/src/platform.ts
CHANGED
|
@@ -608,6 +608,20 @@ export async function getProcessBatchResources(pids: number[]): Promise<Map<numb
|
|
|
608
608
|
}) ?? new Map();
|
|
609
609
|
}
|
|
610
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Parse Unix lsof LISTEN output and return only true listening TCP ports.
|
|
613
|
+
*/
|
|
614
|
+
export function parseUnixListeningPorts(output: string): number[] {
|
|
615
|
+
const ports = new Set<number>();
|
|
616
|
+
for (const line of output.split('\n')) {
|
|
617
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
618
|
+
if (portMatch) {
|
|
619
|
+
ports.add(parseInt(portMatch[1]));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return Array.from(ports);
|
|
623
|
+
}
|
|
624
|
+
|
|
611
625
|
/**
|
|
612
626
|
* Get the TCP ports a process is currently listening on by querying the OS.
|
|
613
627
|
* Returns an array of port numbers (empty if none or process not found).
|
|
@@ -643,14 +657,7 @@ export async function getProcessPorts(pid: number): Promise<number[]> {
|
|
|
643
657
|
} catch { /* ss not available, try lsof */ }
|
|
644
658
|
|
|
645
659
|
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
646
|
-
|
|
647
|
-
for (const line of result.split('\n')) {
|
|
648
|
-
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
649
|
-
if (portMatch) {
|
|
650
|
-
ports.add(parseInt(portMatch[1]));
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return Array.from(ports);
|
|
660
|
+
return parseUnixListeningPorts(result);
|
|
654
661
|
}
|
|
655
662
|
} catch {
|
|
656
663
|
return [];
|
package/src/bgrun.test.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bgrun core utility tests
|
|
3
|
-
*
|
|
4
|
-
* Tests pure logic functions: env parsing, config flattening,
|
|
5
|
-
* string truncation, and runtime calculation.
|
|
6
|
-
*
|
|
7
|
-
* Run: bun test src/bgrun.test.ts
|
|
8
|
-
*/
|
|
9
|
-
import { describe, expect, test } from 'bun:test'
|
|
10
|
-
import { parseEnvString, calculateRuntime } from './utils'
|
|
11
|
-
import { stripAnsi, truncateString, truncatePath } from './table'
|
|
12
|
-
import { detectPackageManager, formatDeployToolError } from './deploy'
|
|
13
|
-
import { isProcessRunning } from './platform'
|
|
14
|
-
import { mkdirSync, rmSync } from 'fs'
|
|
15
|
-
|
|
16
|
-
// Use a test-specific database to avoid polluting real data
|
|
17
|
-
process.env.BGRUN_DB = `bgrun-test-${Date.now()}.sqlite`
|
|
18
|
-
import { addDependency, removeDependency, getDependencyGraph, getDependencies, getDependents, getStartOrder, removeAllDependencies } from './db'
|
|
19
|
-
|
|
20
|
-
// ─── parseEnvString ─────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
describe('parseEnvString', () => {
|
|
23
|
-
test('parses comma-separated key=value pairs', () => {
|
|
24
|
-
const result = parseEnvString('PORT=3000,HOST=localhost,DEBUG=true')
|
|
25
|
-
expect(result).toEqual({
|
|
26
|
-
PORT: '3000',
|
|
27
|
-
HOST: 'localhost',
|
|
28
|
-
DEBUG: 'true',
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('handles single pair', () => {
|
|
33
|
-
expect(parseEnvString('KEY=value')).toEqual({ KEY: 'value' })
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('handles empty string', () => {
|
|
37
|
-
expect(parseEnvString('')).toEqual({})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('ignores malformed pairs (no =)', () => {
|
|
41
|
-
const result = parseEnvString('GOOD=yes,BAD,ALSO_GOOD=ok')
|
|
42
|
-
expect(result.GOOD).toBe('yes')
|
|
43
|
-
expect(result.ALSO_GOOD).toBe('ok')
|
|
44
|
-
expect(result.BAD).toBeUndefined()
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// ─── calculateRuntime ───────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe('calculateRuntime', () => {
|
|
51
|
-
test('returns 0 minutes for recent start', () => {
|
|
52
|
-
const now = new Date().toISOString()
|
|
53
|
-
expect(calculateRuntime(now)).toBe('0 minutes')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('returns correct minutes', () => {
|
|
57
|
-
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString()
|
|
58
|
-
expect(calculateRuntime(fiveMinAgo)).toBe('5 minutes')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('returns correct for 1 hour', () => {
|
|
62
|
-
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
|
|
63
|
-
expect(calculateRuntime(oneHourAgo)).toBe('60 minutes')
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// ─── stripAnsi ──────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
describe('stripAnsi', () => {
|
|
70
|
-
test('strips color codes', () => {
|
|
71
|
-
const colored = '\u001b[31mred text\u001b[0m'
|
|
72
|
-
expect(stripAnsi(colored)).toBe('red text')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('passes through plain text', () => {
|
|
76
|
-
expect(stripAnsi('hello world')).toBe('hello world')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('handles empty string', () => {
|
|
80
|
-
expect(stripAnsi('')).toBe('')
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// ─── truncateString ─────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
describe('truncateString', () => {
|
|
87
|
-
test('returns string unchanged if within limit', () => {
|
|
88
|
-
expect(truncateString('hello', 10)).toBe('hello')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('truncates with ellipsis', () => {
|
|
92
|
-
const result = truncateString('a very long string that exceeds limit', 15)
|
|
93
|
-
expect(result.length).toBeLessThanOrEqual(15)
|
|
94
|
-
expect(result).toContain('…')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('handles maxLength smaller than ellipsis', () => {
|
|
98
|
-
const result = truncateString('hello world', 2)
|
|
99
|
-
expect(result.length).toBeLessThanOrEqual(2)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// ─── truncatePath ───────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
describe('truncatePath', () => {
|
|
106
|
-
test('returns path unchanged if within limit', () => {
|
|
107
|
-
expect(truncatePath('/home/user', 50)).toBe('/home/user')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('truncates middle of long path', () => {
|
|
111
|
-
const longPath = '/home/user/projects/very/deeply/nested/directory/structure'
|
|
112
|
-
const result = truncatePath(longPath, 30)
|
|
113
|
-
expect(result.length).toBeLessThanOrEqual(30)
|
|
114
|
-
expect(result).toContain('…')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// ─── detectPackageManager ───────────────────────────────
|
|
119
|
-
|
|
120
|
-
// ─── isProcessRunning (Windows liveness fallback) ───────
|
|
121
|
-
|
|
122
|
-
describe('isProcessRunning', () => {
|
|
123
|
-
test('returns true for the current process PID', async () => {
|
|
124
|
-
const alive = await isProcessRunning(process.pid)
|
|
125
|
-
expect(alive).toBe(true)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('returns false for PID 0 (intentionally stopped)', async () => {
|
|
129
|
-
const alive = await isProcessRunning(0)
|
|
130
|
-
expect(alive).toBe(false)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('returns false for a very high unlikely PID', async () => {
|
|
134
|
-
const alive = await isProcessRunning(999999)
|
|
135
|
-
expect(alive).toBe(false)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('returns false for negative PID', async () => {
|
|
139
|
-
const alive = await isProcessRunning(-1)
|
|
140
|
-
expect(alive).toBe(false)
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// ─── detectPackageManager ───────────────────────────────
|
|
145
|
-
|
|
146
|
-
describe('formatDeployToolError', () => {
|
|
147
|
-
test('returns actionable message for missing binary', () => {
|
|
148
|
-
const msg = formatDeployToolError('pnpm', new Error('command not found: pnpm'))
|
|
149
|
-
expect(msg).toContain("requires 'pnpm'")
|
|
150
|
-
expect(msg).toContain('PATH')
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
test('preserves non-missing-binary failures', () => {
|
|
154
|
-
const msg = formatDeployToolError('npm', new Error('npm ci failed with exit code 1'))
|
|
155
|
-
expect(msg).toContain('Dependency install failed with npm')
|
|
156
|
-
expect(msg).toContain('exit code 1')
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('detectPackageManager', () => {
|
|
161
|
-
test('returns null when no package.json exists', async () => {
|
|
162
|
-
const dir = `${process.cwd()}/tmp-no-package-${Date.now()}`
|
|
163
|
-
mkdirSync(dir, { recursive: true })
|
|
164
|
-
try {
|
|
165
|
-
expect(await detectPackageManager(dir)).toBeNull()
|
|
166
|
-
} finally {
|
|
167
|
-
rmSync(dir, { recursive: true, force: true })
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('prefers bun lockfiles', async () => {
|
|
172
|
-
const dir = `${process.cwd()}/tmp-bun-${Date.now()}`
|
|
173
|
-
mkdirSync(dir, { recursive: true })
|
|
174
|
-
try {
|
|
175
|
-
await Bun.write(`${dir}/package.json`, '{}')
|
|
176
|
-
await Bun.write(`${dir}/bun.lock`, '')
|
|
177
|
-
expect(await detectPackageManager(dir)).toBe('bun')
|
|
178
|
-
} finally {
|
|
179
|
-
rmSync(dir, { recursive: true, force: true })
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
test('detects pnpm, yarn, and npm lockfiles', async () => {
|
|
184
|
-
const base = `${process.cwd()}/tmp-pm-${Date.now()}`
|
|
185
|
-
|
|
186
|
-
const pnpmDir = `${base}-pnpm`
|
|
187
|
-
mkdirSync(pnpmDir, { recursive: true })
|
|
188
|
-
await Bun.write(`${pnpmDir}/package.json`, '{}')
|
|
189
|
-
await Bun.write(`${pnpmDir}/pnpm-lock.yaml`, '')
|
|
190
|
-
expect(await detectPackageManager(pnpmDir)).toBe('pnpm')
|
|
191
|
-
|
|
192
|
-
const yarnDir = `${base}-yarn`
|
|
193
|
-
mkdirSync(yarnDir, { recursive: true })
|
|
194
|
-
await Bun.write(`${yarnDir}/package.json`, '{}')
|
|
195
|
-
await Bun.write(`${yarnDir}/yarn.lock`, '')
|
|
196
|
-
expect(await detectPackageManager(yarnDir)).toBe('yarn')
|
|
197
|
-
|
|
198
|
-
const npmDir = `${base}-npm`
|
|
199
|
-
mkdirSync(npmDir, { recursive: true })
|
|
200
|
-
await Bun.write(`${npmDir}/package.json`, '{}')
|
|
201
|
-
await Bun.write(`${npmDir}/package-lock.json`, '{}')
|
|
202
|
-
expect(await detectPackageManager(npmDir)).toBe('npm')
|
|
203
|
-
|
|
204
|
-
rmSync(pnpmDir, { recursive: true, force: true })
|
|
205
|
-
rmSync(yarnDir, { recursive: true, force: true })
|
|
206
|
-
rmSync(npmDir, { recursive: true, force: true })
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
test('defaults to bun for package.json projects without a lockfile', async () => {
|
|
210
|
-
const dir = `${process.cwd()}/tmp-default-bun-${Date.now()}`
|
|
211
|
-
mkdirSync(dir, { recursive: true })
|
|
212
|
-
try {
|
|
213
|
-
await Bun.write(`${dir}/package.json`, '{}')
|
|
214
|
-
expect(await detectPackageManager(dir)).toBe('bun')
|
|
215
|
-
} finally {
|
|
216
|
-
rmSync(dir, { recursive: true, force: true })
|
|
217
|
-
}
|
|
218
|
-
})
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
// ─── Dependencies ───────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
describe('addDependency', () => {
|
|
224
|
-
test('adds a valid dependency', () => {
|
|
225
|
-
removeAllDependencies('web-server');
|
|
226
|
-
removeAllDependencies('database');
|
|
227
|
-
const ok = addDependency('web-server', 'database');
|
|
228
|
-
expect(ok).toBe(true);
|
|
229
|
-
expect(getDependencies('web-server')).toContain('database');
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('prevents self-dependency', () => {
|
|
233
|
-
expect(addDependency('api', 'api')).toBe(false);
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
test('prevents duplicate dependency', () => {
|
|
237
|
-
removeAllDependencies('app');
|
|
238
|
-
addDependency('app', 'db');
|
|
239
|
-
expect(addDependency('app', 'db')).toBe(false);
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test('prevents circular dependency', () => {
|
|
243
|
-
removeAllDependencies('a');
|
|
244
|
-
removeAllDependencies('b');
|
|
245
|
-
removeAllDependencies('c');
|
|
246
|
-
addDependency('a', 'b');
|
|
247
|
-
addDependency('b', 'c');
|
|
248
|
-
// c -> a would create a cycle
|
|
249
|
-
expect(addDependency('c', 'a')).toBe(false);
|
|
250
|
-
})
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
describe('getDependencyGraph', () => {
|
|
254
|
-
test('returns full graph', () => {
|
|
255
|
-
removeAllDependencies('svc-a');
|
|
256
|
-
removeAllDependencies('svc-b');
|
|
257
|
-
addDependency('svc-a', 'svc-b');
|
|
258
|
-
const graph = getDependencyGraph();
|
|
259
|
-
expect(graph['svc-a']).toContain('svc-b');
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
describe('getDependents', () => {
|
|
264
|
-
test('finds processes that depend on a target', () => {
|
|
265
|
-
removeAllDependencies('frontend');
|
|
266
|
-
removeAllDependencies('backend');
|
|
267
|
-
addDependency('frontend', 'backend');
|
|
268
|
-
expect(getDependents('backend')).toContain('frontend');
|
|
269
|
-
})
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
describe('removeDependency', () => {
|
|
273
|
-
test('removes an existing dependency', () => {
|
|
274
|
-
removeAllDependencies('x');
|
|
275
|
-
addDependency('x', 'y');
|
|
276
|
-
expect(getDependencies('x')).toContain('y');
|
|
277
|
-
removeDependency('x', 'y');
|
|
278
|
-
expect(getDependencies('x')).not.toContain('y');
|
|
279
|
-
})
|
|
280
|
-
})
|
package/src/index_copy.ts
DELETED
|
@@ -1,614 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { parseArgs } from "util";
|
|
4
|
-
import { getVersion } from "./utils";
|
|
5
|
-
import { handleRun } from "./commands/run";
|
|
6
|
-
import { showAll } from "./commands/list";
|
|
7
|
-
import { handleDelete, handleClean, handleDeleteAll, handleStop } from "./commands/cleanup";
|
|
8
|
-
import { handleWatch } from "./commands/watch";
|
|
9
|
-
import { showLogs } from "./commands/logs";
|
|
10
|
-
import { showDetails } from "./commands/details";
|
|
11
|
-
import type { CommandOptions } from "./types";
|
|
12
|
-
import { error, announce } from "./logger";
|
|
13
|
-
// startServer is dynamically imported only when --_serve is used
|
|
14
|
-
// to avoid loading melina (which has side-effects) on every bgrun command
|
|
15
|
-
import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree, isPortFree, findPidByPort } from "./platform";
|
|
16
|
-
import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation, getDbInfo } from "./db";
|
|
17
|
-
import dedent from "dedent";
|
|
18
|
-
import chalk from "chalk";
|
|
19
|
-
import { join } from "path";
|
|
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
|
-
}
|
|
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
|
-
|
|
71
|
-
async function showHelp() {
|
|
72
|
-
const usage = dedent`
|
|
73
|
-
${chalk.bold('bgrun — Bun Background Runner')}
|
|
74
|
-
${chalk.gray('═'.repeat(50))}
|
|
75
|
-
|
|
76
|
-
${chalk.yellow('Usage:')}
|
|
77
|
-
bgrun [name] [options]
|
|
78
|
-
|
|
79
|
-
${chalk.yellow('Commands:')}
|
|
80
|
-
bgrun List all processes
|
|
81
|
-
bgrun [name] Show details for a process
|
|
82
|
-
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
83
|
-
bgrun --guard Launch standalone process guard
|
|
84
|
-
bgrun --restart [name] Restart a process
|
|
85
|
-
bgrun --restart-all Restart ALL registered processes
|
|
86
|
-
bgrun --stop [name] Stop a process (keep in registry)
|
|
87
|
-
bgrun --stop-all Stop ALL running processes
|
|
88
|
-
bgrun --delete [name] Delete a process
|
|
89
|
-
bgrun --clean Remove all stopped processes
|
|
90
|
-
bgrun --nuke Delete ALL processes
|
|
91
|
-
|
|
92
|
-
${chalk.yellow('Options:')}
|
|
93
|
-
--name <string> Process name (required for new)
|
|
94
|
-
--command <string> Process command (required for new)
|
|
95
|
-
--directory <path> Working directory (required for new)
|
|
96
|
-
--config <path> Config file (default: .config.toml)
|
|
97
|
-
--watch Watch for file changes and auto-restart
|
|
98
|
-
--force Force restart existing process
|
|
99
|
-
--fetch Fetch latest git changes before running
|
|
100
|
-
--json Output in JSON format
|
|
101
|
-
--filter <group> Filter list by BGR_GROUP
|
|
102
|
-
--logs Show logs
|
|
103
|
-
--log-stdout Show only stdout logs
|
|
104
|
-
--log-stderr Show only stderr logs
|
|
105
|
-
--lines <n> Number of log lines to show (default: all)
|
|
106
|
-
--version Show version
|
|
107
|
-
--debug Show debug info (DB path, BGR home, etc.)
|
|
108
|
-
--dashboard Launch web dashboard as bgrun-managed process
|
|
109
|
-
--port <number> Port for dashboard (default: 3000)
|
|
110
|
-
--help Show this help message
|
|
111
|
-
|
|
112
|
-
${chalk.yellow('Examples:')}
|
|
113
|
-
bgrun --dashboard
|
|
114
|
-
bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
115
|
-
bgrun myapp --logs --lines 50
|
|
116
|
-
`;
|
|
117
|
-
console.log(usage);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Re-running parseArgs logic properly
|
|
121
|
-
async function run() {
|
|
122
|
-
const { values, positionals } = parseArgs({
|
|
123
|
-
args: Bun.argv.slice(2),
|
|
124
|
-
options: {
|
|
125
|
-
name: { type: 'string' },
|
|
126
|
-
command: { type: 'string' },
|
|
127
|
-
directory: { type: 'string' },
|
|
128
|
-
config: { type: 'string' },
|
|
129
|
-
watch: { type: 'boolean' },
|
|
130
|
-
force: { type: 'boolean' },
|
|
131
|
-
fetch: { type: 'boolean' },
|
|
132
|
-
delete: { type: 'boolean' },
|
|
133
|
-
nuke: { type: 'boolean' },
|
|
134
|
-
restart: { type: 'boolean' },
|
|
135
|
-
"restart-all": { type: 'boolean' },
|
|
136
|
-
stop: { type: 'boolean' },
|
|
137
|
-
"stop-all": { type: 'boolean' },
|
|
138
|
-
clean: { type: 'boolean' },
|
|
139
|
-
json: { type: 'boolean' },
|
|
140
|
-
logs: { type: 'boolean' },
|
|
141
|
-
"log-stdout": { type: 'boolean' },
|
|
142
|
-
"log-stderr": { type: 'boolean' },
|
|
143
|
-
lines: { type: 'string' },
|
|
144
|
-
filter: { type: 'string' },
|
|
145
|
-
version: { type: 'boolean' },
|
|
146
|
-
help: { type: 'boolean' },
|
|
147
|
-
db: { type: 'string' },
|
|
148
|
-
stdout: { type: 'string' },
|
|
149
|
-
stderr: { type: 'string' },
|
|
150
|
-
dashboard: { type: 'boolean' },
|
|
151
|
-
guard: { type: 'boolean' },
|
|
152
|
-
debug: { type: 'boolean' },
|
|
153
|
-
"_serve": { type: 'boolean' },
|
|
154
|
-
"_guard-loop": { type: 'boolean' },
|
|
155
|
-
port: { type: 'string' },
|
|
156
|
-
},
|
|
157
|
-
strict: false,
|
|
158
|
-
allowPositionals: true,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Internal: actually run the HTTP server (spawned by --dashboard)
|
|
162
|
-
// Port is NOT passed explicitly — Melina auto-detects from BUN_PORT env
|
|
163
|
-
// or defaults to 3000 with fallback to next available port.
|
|
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();
|
|
168
|
-
const { startServer } = await import("./server");
|
|
169
|
-
await startServer();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Internal: actually run the guard loop (spawned by --guard)
|
|
174
|
-
if (values['_guard-loop']) {
|
|
175
|
-
// Redirect console output to log files when running detached
|
|
176
|
-
redirectConsoleToFiles();
|
|
177
|
-
const { startGuardLoop } = await import("./guard");
|
|
178
|
-
const intervalStr = positionals[0];
|
|
179
|
-
const intervalMs = intervalStr ? parseInt(intervalStr) * 1000 : undefined;
|
|
180
|
-
await startGuardLoop(intervalMs);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Dashboard: spawn the dashboard server as a bgr-managed process
|
|
185
|
-
if (values.dashboard) {
|
|
186
|
-
const dashboardName = 'bgr-dashboard';
|
|
187
|
-
const homePath = getHomeDir();
|
|
188
|
-
const bgrDir = join(homePath, '.bgr');
|
|
189
|
-
// User can request a specific port via BUN_PORT=XXXX bgrun --dashboard
|
|
190
|
-
// Otherwise Melina picks automatically (3000 → fallback)
|
|
191
|
-
const requestedPort = values.port as string | undefined;
|
|
192
|
-
|
|
193
|
-
// Check if dashboard is already running
|
|
194
|
-
const existing = getProcess(dashboardName);
|
|
195
|
-
if (existing && await isProcessRunning(existing.pid)) {
|
|
196
|
-
// The stored PID may be the shell wrapper (cmd.exe), not the actual bun process
|
|
197
|
-
// Try the stored PID first, then traverse the process tree to find the real one
|
|
198
|
-
let existingPorts = await getProcessPorts(existing.pid);
|
|
199
|
-
if (existingPorts.length === 0) {
|
|
200
|
-
const childPid = await findChildPid(existing.pid);
|
|
201
|
-
if (childPid !== existing.pid) {
|
|
202
|
-
existingPorts = await getProcessPorts(childPid);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : '(detecting...)';
|
|
206
|
-
announce(
|
|
207
|
-
`Dashboard is already running (PID ${existing.pid})\n\n` +
|
|
208
|
-
` 🌐 ${chalk.cyan(`http://localhost${portStr}`)}\n\n` +
|
|
209
|
-
` Use ${chalk.yellow(`bgrun --stop ${dashboardName}`)} to stop it\n` +
|
|
210
|
-
` Use ${chalk.yellow(`bgrun --dashboard --force`)} to restart`,
|
|
211
|
-
'BGR Dashboard'
|
|
212
|
-
);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Kill existing if force
|
|
217
|
-
if (existing) {
|
|
218
|
-
if (await isProcessRunning(existing.pid)) {
|
|
219
|
-
const detectedPorts = await getProcessPorts(existing.pid);
|
|
220
|
-
await terminateProcess(existing.pid);
|
|
221
|
-
for (const p of detectedPorts) {
|
|
222
|
-
await killProcessOnPort(p);
|
|
223
|
-
await waitForPortFree(p, 5000);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
await retryDatabaseOperation(() => removeProcessByName(dashboardName));
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Spawn the dashboard server as a managed process
|
|
230
|
-
// Port is NOT passed as CLI arg — Melina will auto-detect.
|
|
231
|
-
// If user wants a specific port, we pass it via BUN_PORT env var.
|
|
232
|
-
const { resolve } = require('path');
|
|
233
|
-
const scriptPath = resolve(process.argv[1]);
|
|
234
|
-
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
235
|
-
const command = `bgrun --_serve`;
|
|
236
|
-
const stdoutPath = join(bgrDir, `${dashboardName}-out.txt`);
|
|
237
|
-
const stderrPath = join(bgrDir, `${dashboardName}-err.txt`);
|
|
238
|
-
|
|
239
|
-
await Bun.write(stdoutPath, '');
|
|
240
|
-
await Bun.write(stderrPath, '');
|
|
241
|
-
|
|
242
|
-
// Pass BUN_PORT env var only if user explicitly requested a port
|
|
243
|
-
const spawnEnv: Record<string, string> = { ...Bun.env } as any;
|
|
244
|
-
if (requestedPort) {
|
|
245
|
-
spawnEnv.BUN_PORT = requestedPort;
|
|
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;
|
|
250
|
-
|
|
251
|
-
// Resolve the target port: --port flag > BUN_PORT env > default 3000
|
|
252
|
-
const targetPort = parseInt(requestedPort || Bun.env.BUN_PORT || '3000');
|
|
253
|
-
if (!isNaN(targetPort) && targetPort > 0) {
|
|
254
|
-
// Auto-kill whatever occupies the target port so dashboard always reclaims it
|
|
255
|
-
const portFree = await isPortFree(targetPort);
|
|
256
|
-
if (!portFree) {
|
|
257
|
-
console.log(chalk.yellow(` ⚡ Port ${targetPort} is occupied — reclaiming...`));
|
|
258
|
-
await killProcessOnPort(targetPort);
|
|
259
|
-
const freed = await waitForPortFree(targetPort, 5000);
|
|
260
|
-
if (!freed) {
|
|
261
|
-
console.log(chalk.red(` ⚠ Could not free port ${targetPort} — dashboard may pick a fallback port`));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
267
|
-
env: spawnEnv,
|
|
268
|
-
cwd: bgrDir,
|
|
269
|
-
stdout: "ignore",
|
|
270
|
-
stderr: "ignore",
|
|
271
|
-
detached: true, // Windows: new process group outside parent's Job Object — survives terminal close
|
|
272
|
-
} as any);
|
|
273
|
-
|
|
274
|
-
newProcess.unref();
|
|
275
|
-
|
|
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.
|
|
278
|
-
await sleep(2000); // Give the server time to start and bind a port
|
|
279
|
-
const resolvedPort = parseInt(requestedPort || Bun.env.BUN_PORT || '3000');
|
|
280
|
-
const actualPid = await findPidByPort(resolvedPort, 10000) ?? await findChildPid(newProcess.pid);
|
|
281
|
-
|
|
282
|
-
// Detect the port the server actually bound to
|
|
283
|
-
let actualPort: number | null = null;
|
|
284
|
-
for (let attempt = 0; attempt < 10; attempt++) {
|
|
285
|
-
const ports = await getProcessPorts(actualPid);
|
|
286
|
-
if (ports.length > 0) {
|
|
287
|
-
actualPort = ports[0];
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
await sleep(1000);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
await retryDatabaseOperation(() =>
|
|
294
|
-
insertProcess({
|
|
295
|
-
pid: actualPid,
|
|
296
|
-
workdir: bgrDir,
|
|
297
|
-
command,
|
|
298
|
-
name: dashboardName,
|
|
299
|
-
env: '',
|
|
300
|
-
configPath: '',
|
|
301
|
-
stdout_path: stdoutPath,
|
|
302
|
-
stderr_path: stderrPath,
|
|
303
|
-
})
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
const portDisplay = actualPort ? String(actualPort) : '(detecting...)';
|
|
307
|
-
const urlDisplay = actualPort ? `http://localhost:${actualPort}` : 'http://localhost (port auto-assigned)';
|
|
308
|
-
|
|
309
|
-
const msg = dedent`
|
|
310
|
-
${chalk.bold('⚡ BGR Dashboard launched')}
|
|
311
|
-
${chalk.gray('─'.repeat(40))}
|
|
312
|
-
|
|
313
|
-
🌐 Open in browser: ${chalk.cyan.underline(urlDisplay)}
|
|
314
|
-
📊 Manage all your processes from the web UI
|
|
315
|
-
🔄 Auto-refreshes every 3 seconds
|
|
316
|
-
|
|
317
|
-
${chalk.gray('─'.repeat(40))}
|
|
318
|
-
Process: ${chalk.white(dashboardName)} | PID: ${chalk.white(String(actualPid))} | Port: ${chalk.white(portDisplay)}
|
|
319
|
-
|
|
320
|
-
${chalk.yellow('bgrun bgr-dashboard --logs')} View dashboard logs
|
|
321
|
-
${chalk.yellow('bgrun --stop bgr-dashboard')} Stop the dashboard
|
|
322
|
-
${chalk.yellow('bgrun --restart bgr-dashboard')} Restart the dashboard
|
|
323
|
-
`;
|
|
324
|
-
announce(msg, 'BGR Dashboard');
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Guard: spawn the standalone guard as a bgr-managed process
|
|
329
|
-
if (values.guard) {
|
|
330
|
-
const guardName = 'bgr-guard';
|
|
331
|
-
const homePath = getHomeDir();
|
|
332
|
-
const bgrDir = join(homePath, '.bgr');
|
|
333
|
-
|
|
334
|
-
// Check if guard is already running
|
|
335
|
-
const existing = getProcess(guardName);
|
|
336
|
-
if (existing && await isProcessRunning(existing.pid)) {
|
|
337
|
-
announce(
|
|
338
|
-
`Guard is already running (PID ${existing.pid})\n\n` +
|
|
339
|
-
` Use ${chalk.yellow(`bgrun --stop ${guardName}`)} to stop it\n` +
|
|
340
|
-
` Use ${chalk.yellow(`bgrun --guard --force`)} to restart`,
|
|
341
|
-
'BGR Guard'
|
|
342
|
-
);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Kill existing if force
|
|
347
|
-
if (existing) {
|
|
348
|
-
if (await isProcessRunning(existing.pid)) {
|
|
349
|
-
await terminateProcess(existing.pid);
|
|
350
|
-
}
|
|
351
|
-
await retryDatabaseOperation(() => removeProcessByName(guardName));
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const { resolve } = require('path');
|
|
355
|
-
const scriptPath = resolve(process.argv[1]);
|
|
356
|
-
const spawnCommand = `bun run ${scriptPath} --_guard-loop`;
|
|
357
|
-
const command = `bgrun --_guard-loop`;
|
|
358
|
-
const stdoutPath = join(bgrDir, `${guardName}-out.txt`);
|
|
359
|
-
const stderrPath = join(bgrDir, `${guardName}-err.txt`);
|
|
360
|
-
|
|
361
|
-
await Bun.write(stdoutPath, '');
|
|
362
|
-
await Bun.write(stderrPath, '');
|
|
363
|
-
|
|
364
|
-
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
365
|
-
env: { ...Bun.env, BGR_STDOUT: stdoutPath, BGR_STDERR: stderrPath },
|
|
366
|
-
cwd: bgrDir,
|
|
367
|
-
stdout: "ignore",
|
|
368
|
-
stderr: "ignore",
|
|
369
|
-
detached: true, // Windows: new process group outside parent's Job Object — survives terminal close
|
|
370
|
-
} as any);
|
|
371
|
-
|
|
372
|
-
newProcess.unref();
|
|
373
|
-
await sleep(1000);
|
|
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
|
-
}
|
|
386
|
-
|
|
387
|
-
await retryDatabaseOperation(() =>
|
|
388
|
-
insertProcess({
|
|
389
|
-
pid: actualPid,
|
|
390
|
-
workdir: bgrDir,
|
|
391
|
-
command,
|
|
392
|
-
name: guardName,
|
|
393
|
-
env: 'BGR_KEEP_ALIVE=false', // Guard doesn't guard itself
|
|
394
|
-
configPath: '',
|
|
395
|
-
stdout_path: stdoutPath,
|
|
396
|
-
stderr_path: stderrPath,
|
|
397
|
-
})
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
const msg = dedent`
|
|
401
|
-
${chalk.bold('🛡️ BGR Standalone Guard launched')}
|
|
402
|
-
${chalk.gray('─'.repeat(40))}
|
|
403
|
-
|
|
404
|
-
Monitors: All processes with BGR_KEEP_ALIVE=true
|
|
405
|
-
Also watches: bgr-dashboard (auto-restart if it dies)
|
|
406
|
-
Check interval: 30 seconds
|
|
407
|
-
Backoff: Exponential after 5 rapid crashes
|
|
408
|
-
|
|
409
|
-
${chalk.gray('─'.repeat(40))}
|
|
410
|
-
Process: ${chalk.white(guardName)} | PID: ${chalk.white(String(actualPid))}
|
|
411
|
-
|
|
412
|
-
${chalk.yellow(`bgrun ${guardName} --logs`)} View guard logs
|
|
413
|
-
${chalk.yellow(`bgrun --stop ${guardName}`)} Stop the guard
|
|
414
|
-
`;
|
|
415
|
-
announce(msg, 'BGR Guard');
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (values.version) {
|
|
420
|
-
console.log(`bgrun version: ${await getVersion()}`);
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (values.help) {
|
|
425
|
-
await showHelp();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
if (values.debug) {
|
|
430
|
-
const info = getDbInfo();
|
|
431
|
-
const version = await getVersion();
|
|
432
|
-
console.log(dedent`
|
|
433
|
-
${chalk.bold('bgrun debug info')}
|
|
434
|
-
${chalk.gray('─'.repeat(40))}
|
|
435
|
-
Version: ${chalk.cyan(version)}
|
|
436
|
-
BGR Home: ${chalk.yellow(info.bgrHome)}
|
|
437
|
-
DB Path: ${chalk.yellow(info.dbPath)}
|
|
438
|
-
DB File: ${info.dbFilename}
|
|
439
|
-
DB Exists: ${info.exists ? chalk.green('✓') : chalk.red('✗')}
|
|
440
|
-
Platform: ${process.platform}
|
|
441
|
-
Bun: ${Bun.version}
|
|
442
|
-
`);
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Commands flow
|
|
447
|
-
if (values.nuke) {
|
|
448
|
-
await handleDeleteAll();
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (values.clean) {
|
|
453
|
-
await handleClean();
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Restart all registered processes
|
|
458
|
-
if (values['restart-all']) {
|
|
459
|
-
const { getAllProcesses } = await import('./db');
|
|
460
|
-
const all = getAllProcesses();
|
|
461
|
-
if (all.length === 0) {
|
|
462
|
-
error('No processes registered.');
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
console.log(chalk.bold(`\n Restarting ${all.length} processes...\n`));
|
|
466
|
-
for (const proc of all) {
|
|
467
|
-
try {
|
|
468
|
-
console.log(chalk.yellow(` ↻ Restarting ${proc.name}...`));
|
|
469
|
-
await handleRun({
|
|
470
|
-
action: 'run',
|
|
471
|
-
name: proc.name,
|
|
472
|
-
force: true,
|
|
473
|
-
remoteName: '',
|
|
474
|
-
});
|
|
475
|
-
} catch (err: any) {
|
|
476
|
-
console.error(chalk.red(` ✗ Failed to restart ${proc.name}: ${err.message}`));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
console.log(chalk.green(`\n ✓ All processes restarted.\n`));
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Stop all running processes
|
|
484
|
-
if (values['stop-all']) {
|
|
485
|
-
const { getAllProcesses } = await import('./db');
|
|
486
|
-
const all = getAllProcesses();
|
|
487
|
-
if (all.length === 0) {
|
|
488
|
-
error('No processes registered.');
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
console.log(chalk.bold(`\n Stopping ${all.length} processes...\n`));
|
|
492
|
-
for (const proc of all) {
|
|
493
|
-
try {
|
|
494
|
-
if (await isProcessRunning(proc.pid)) {
|
|
495
|
-
console.log(chalk.yellow(` ■ Stopping ${proc.name} (PID ${proc.pid})...`));
|
|
496
|
-
await handleStop(proc.name);
|
|
497
|
-
} else {
|
|
498
|
-
console.log(chalk.gray(` ○ ${proc.name} already stopped`));
|
|
499
|
-
}
|
|
500
|
-
} catch (err: any) {
|
|
501
|
-
console.error(chalk.red(` ✗ Failed to stop ${proc.name}: ${err.message}`));
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
console.log(chalk.green(`\n ✓ All processes stopped.\n`));
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const name = (values.name as string) || positionals[0];
|
|
509
|
-
|
|
510
|
-
// Delete
|
|
511
|
-
if (values.delete) {
|
|
512
|
-
// bgr --delete (bool)
|
|
513
|
-
if (name) {
|
|
514
|
-
await handleDelete(name);
|
|
515
|
-
} else {
|
|
516
|
-
error("Please specify a process name to delete.");
|
|
517
|
-
}
|
|
518
|
-
return;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Restart
|
|
522
|
-
if (values.restart) {
|
|
523
|
-
if (!name) {
|
|
524
|
-
error("Please specify a process name to restart.");
|
|
525
|
-
}
|
|
526
|
-
await handleRun({
|
|
527
|
-
action: 'run',
|
|
528
|
-
name: name,
|
|
529
|
-
force: true,
|
|
530
|
-
// other options undefined, handleRun will look up process
|
|
531
|
-
remoteName: '',
|
|
532
|
-
});
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Stop
|
|
537
|
-
if (values.stop) {
|
|
538
|
-
if (!name) {
|
|
539
|
-
error("Please specify a process name to stop.");
|
|
540
|
-
}
|
|
541
|
-
await handleStop(name);
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Logs
|
|
546
|
-
if (values.logs || values["log-stdout"] || values["log-stderr"]) {
|
|
547
|
-
if (!name) {
|
|
548
|
-
error("Please specify a process name to show logs for.");
|
|
549
|
-
}
|
|
550
|
-
const logType = values["log-stdout"] ? 'stdout' : (values["log-stderr"] ? 'stderr' : 'both');
|
|
551
|
-
const lines = values.lines ? parseInt(values.lines as string) : undefined;
|
|
552
|
-
await showLogs(name, logType, lines);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Watch
|
|
557
|
-
if (values.watch) {
|
|
558
|
-
await handleWatch({
|
|
559
|
-
action: 'watch',
|
|
560
|
-
name: name,
|
|
561
|
-
command: values.command as string | undefined,
|
|
562
|
-
directory: values.directory as string | undefined,
|
|
563
|
-
configPath: values.config as string | undefined,
|
|
564
|
-
force: values.force as boolean | undefined,
|
|
565
|
-
remoteName: '',
|
|
566
|
-
dbPath: values.db as string | undefined,
|
|
567
|
-
stdout: values.stdout as string | undefined,
|
|
568
|
-
stderr: values.stderr as string | undefined
|
|
569
|
-
}, {
|
|
570
|
-
showLogs: (values.logs as boolean) || false,
|
|
571
|
-
logType: 'both',
|
|
572
|
-
lines: values.lines ? parseInt(values.lines as string) : undefined
|
|
573
|
-
});
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// List or Run or Details
|
|
578
|
-
if (name) {
|
|
579
|
-
if (!values.command && !values.directory) {
|
|
580
|
-
await showDetails(name);
|
|
581
|
-
} else {
|
|
582
|
-
await handleRun({
|
|
583
|
-
action: 'run',
|
|
584
|
-
name: name,
|
|
585
|
-
command: values.command as string | undefined,
|
|
586
|
-
directory: values.directory as string | undefined,
|
|
587
|
-
configPath: values.config as string | undefined,
|
|
588
|
-
force: values.force as boolean | undefined,
|
|
589
|
-
fetch: values.fetch as boolean | undefined,
|
|
590
|
-
remoteName: '',
|
|
591
|
-
dbPath: values.db as string | undefined,
|
|
592
|
-
stdout: values.stdout as string | undefined,
|
|
593
|
-
stderr: values.stderr as string | undefined
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
} else {
|
|
597
|
-
if (values.command) {
|
|
598
|
-
error("Process name is required.");
|
|
599
|
-
}
|
|
600
|
-
await showAll({
|
|
601
|
-
json: values.json as boolean | undefined,
|
|
602
|
-
filter: values.filter as string | undefined
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
run().catch(err => {
|
|
608
|
-
// BgrunError was already printed by error() — just exit
|
|
609
|
-
// For unexpected errors, print and exit
|
|
610
|
-
if (err.name !== 'BgrunError') {
|
|
611
|
-
console.error(err);
|
|
612
|
-
}
|
|
613
|
-
process.exit(1);
|
|
614
|
-
});
|