bgrun 3.12.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/app/api/guard/route.ts +4 -1
- package/dashboard/app/api/guard-events/route.ts +5 -0
- package/dashboard/app/api/history/route.ts +39 -0
- package/dashboard/app/api/processes/route.ts +11 -3
- package/dashboard/app/api/restart/[name]/route.ts +7 -0
- package/dashboard/app/api/start/route.ts +5 -0
- package/dashboard/app/api/stop/[name]/route.ts +4 -1
- package/dashboard/app/api/templates/route.ts +47 -0
- package/dashboard/app/globals.css +384 -29
- package/dashboard/app/page.client.tsx +338 -1
- package/dashboard/app/page.tsx +98 -0
- package/dist/index.js +663 -184
- package/package.json +2 -2
- package/scripts/bgr-startup.ps1 +3 -3
- package/scripts/bgrun-startup.ps1 +91 -0
- package/src/commands/details.ts +17 -3
- package/src/commands/list.ts +37 -4
- package/src/commands/run.ts +21 -3
- package/src/db.ts +115 -0
- package/src/guard.ts +51 -0
- package/src/index.ts +83 -14
- package/src/index_copy.ts +614 -0
- package/src/logger.ts +12 -2
- package/src/platform.ts +87 -50
- package/src/server.ts +87 -3
- package/src/utils.ts +2 -2
|
@@ -0,0 +1,614 @@
|
|
|
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
|
+
});
|
package/src/logger.ts
CHANGED
|
@@ -14,7 +14,15 @@ export function announce(message: string, title?: string) {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/** Custom error class so callers can distinguish bgrun errors from unexpected ones */
|
|
18
|
+
export class BgrunError extends Error {
|
|
19
|
+
constructor(message: string) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'BgrunError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function error(message: string | Error): never {
|
|
18
26
|
const text = message instanceof Error ? (message.stack || message.message) : String(message);
|
|
19
27
|
console.error(
|
|
20
28
|
boxen(chalk.red(text), {
|
|
@@ -26,5 +34,7 @@ export function error(message: string | Error) {
|
|
|
26
34
|
borderStyle: 'double'
|
|
27
35
|
})
|
|
28
36
|
);
|
|
29
|
-
process.exit(
|
|
37
|
+
// Throw instead of process.exit() — lets dashboard API handlers catch gracefully
|
|
38
|
+
// CLI entry point has a top-level catch that calls process.exit(1)
|
|
39
|
+
throw new BgrunError(text);
|
|
30
40
|
}
|