bgrun 3.3.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/README.md +720 -0
- package/dashboard/app/api/logs/[name]/route.ts +17 -0
- package/dashboard/app/api/processes/[name]/route.ts +19 -0
- package/dashboard/app/api/processes/route.ts +150 -0
- package/dashboard/app/api/restart/[name]/route.ts +20 -0
- package/dashboard/app/api/start/route.ts +22 -0
- package/dashboard/app/api/stop/[name]/route.ts +16 -0
- package/dashboard/app/api/version/route.ts +8 -0
- package/dashboard/app/globals.css +1135 -0
- package/dashboard/app/layout.tsx +47 -0
- package/dashboard/app/page.client.tsx +554 -0
- package/dashboard/app/page.tsx +130 -0
- package/dist/index.js +1580 -0
- package/examples/bgr-startup.sh +40 -0
- package/package.json +60 -0
- package/src/api.ts +31 -0
- package/src/build.ts +26 -0
- package/src/commands/cleanup.ts +142 -0
- package/src/commands/details.ts +46 -0
- package/src/commands/list.ts +86 -0
- package/src/commands/logs.ts +49 -0
- package/src/commands/run.ts +151 -0
- package/src/commands/watch.ts +223 -0
- package/src/config.ts +37 -0
- package/src/db.ts +115 -0
- package/src/index.ts +349 -0
- package/src/logger.ts +29 -0
- package/src/platform.ts +440 -0
- package/src/schema.ts +2 -0
- package/src/server.ts +24 -0
- package/src/table.ts +230 -0
- package/src/types.ts +27 -0
- package/src/utils.ts +99 -0
- package/src/version.macro.ts +17 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
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
|
+
import { startServer } from "./server";
|
|
14
|
+
import { getHomeDir, getShellCommand, findChildPid, isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort, waitForPortFree } from "./platform";
|
|
15
|
+
import { insertProcess, removeProcessByName, getProcess, retryDatabaseOperation } from "./db";
|
|
16
|
+
import dedent from "dedent";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { sleep } from "bun";
|
|
20
|
+
|
|
21
|
+
async function showHelp() {
|
|
22
|
+
const usage = dedent`
|
|
23
|
+
${chalk.bold('bgrun — Bun Background Runner')}
|
|
24
|
+
${chalk.gray('═'.repeat(50))}
|
|
25
|
+
|
|
26
|
+
${chalk.yellow('Usage:')}
|
|
27
|
+
bgrun [name] [options]
|
|
28
|
+
|
|
29
|
+
${chalk.yellow('Commands:')}
|
|
30
|
+
bgrun List all processes
|
|
31
|
+
bgrun [name] Show details for a process
|
|
32
|
+
bgrun --dashboard Launch web dashboard (managed by bgrun)
|
|
33
|
+
bgrun --restart [name] Restart a process
|
|
34
|
+
bgrun --stop [name] Stop a process (keep in registry)
|
|
35
|
+
bgrun --delete [name] Delete a process
|
|
36
|
+
bgrun --clean Remove all stopped processes
|
|
37
|
+
bgrun --nuke Delete ALL processes
|
|
38
|
+
|
|
39
|
+
${chalk.yellow('Options:')}
|
|
40
|
+
--name <string> Process name (required for new)
|
|
41
|
+
--command <string> Process command (required for new)
|
|
42
|
+
--directory <path> Working directory (required for new)
|
|
43
|
+
--config <path> Config file (default: .config.toml)
|
|
44
|
+
--watch Watch for file changes and auto-restart
|
|
45
|
+
--force Force restart existing process
|
|
46
|
+
--fetch Fetch latest git changes before running
|
|
47
|
+
--json Output in JSON format
|
|
48
|
+
--filter <group> Filter list by BGR_GROUP
|
|
49
|
+
--logs Show logs
|
|
50
|
+
--log-stdout Show only stdout logs
|
|
51
|
+
--log-stderr Show only stderr logs
|
|
52
|
+
--lines <n> Number of log lines to show (default: all)
|
|
53
|
+
--version Show version
|
|
54
|
+
--dashboard Launch web dashboard as bgrun-managed process
|
|
55
|
+
--port <number> Port for dashboard (default: 3000)
|
|
56
|
+
--help Show this help message
|
|
57
|
+
|
|
58
|
+
${chalk.yellow('Examples:')}
|
|
59
|
+
bgrun --dashboard
|
|
60
|
+
bgrun --name myapp --command "bun run dev" --directory . --watch
|
|
61
|
+
bgrun myapp --logs --lines 50
|
|
62
|
+
`;
|
|
63
|
+
console.log(usage);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Re-running parseArgs logic properly
|
|
67
|
+
async function run() {
|
|
68
|
+
const { values, positionals } = parseArgs({
|
|
69
|
+
args: Bun.argv.slice(2),
|
|
70
|
+
options: {
|
|
71
|
+
name: { type: 'string' },
|
|
72
|
+
command: { type: 'string' },
|
|
73
|
+
directory: { type: 'string' },
|
|
74
|
+
config: { type: 'string' },
|
|
75
|
+
watch: { type: 'boolean' },
|
|
76
|
+
force: { type: 'boolean' },
|
|
77
|
+
fetch: { type: 'boolean' },
|
|
78
|
+
delete: { type: 'boolean' },
|
|
79
|
+
nuke: { type: 'boolean' },
|
|
80
|
+
restart: { type: 'boolean' },
|
|
81
|
+
stop: { type: 'boolean' },
|
|
82
|
+
clean: { type: 'boolean' },
|
|
83
|
+
json: { type: 'boolean' },
|
|
84
|
+
logs: { type: 'boolean' },
|
|
85
|
+
"log-stdout": { type: 'boolean' },
|
|
86
|
+
"log-stderr": { type: 'boolean' },
|
|
87
|
+
lines: { type: 'string' },
|
|
88
|
+
filter: { type: 'string' },
|
|
89
|
+
version: { type: 'boolean' },
|
|
90
|
+
help: { type: 'boolean' },
|
|
91
|
+
db: { type: 'string' },
|
|
92
|
+
stdout: { type: 'string' },
|
|
93
|
+
stderr: { type: 'string' },
|
|
94
|
+
dashboard: { type: 'boolean' },
|
|
95
|
+
"_serve": { type: 'boolean' },
|
|
96
|
+
port: { type: 'string' },
|
|
97
|
+
},
|
|
98
|
+
strict: false,
|
|
99
|
+
allowPositionals: true,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Internal: actually run the HTTP server (spawned by --dashboard)
|
|
103
|
+
// Port is NOT passed explicitly — Melina auto-detects from BUN_PORT env
|
|
104
|
+
// or defaults to 3000 with fallback to next available port.
|
|
105
|
+
if (values['_serve']) {
|
|
106
|
+
await startServer();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Dashboard: spawn the dashboard server as a bgr-managed process
|
|
111
|
+
if (values.dashboard) {
|
|
112
|
+
const dashboardName = 'bgr-dashboard';
|
|
113
|
+
const homePath = getHomeDir();
|
|
114
|
+
const bgrDir = join(homePath, '.bgr');
|
|
115
|
+
// User can request a specific port via BUN_PORT=XXXX bgrun --dashboard
|
|
116
|
+
// Otherwise Melina picks automatically (3000 → fallback)
|
|
117
|
+
const requestedPort = values.port as string | undefined;
|
|
118
|
+
|
|
119
|
+
// Check if dashboard is already running
|
|
120
|
+
const existing = getProcess(dashboardName);
|
|
121
|
+
if (existing && await isProcessRunning(existing.pid)) {
|
|
122
|
+
const existingPorts = await getProcessPorts(existing.pid);
|
|
123
|
+
const portStr = existingPorts.length > 0 ? `:${existingPorts[0]}` : '(detecting...)';
|
|
124
|
+
announce(
|
|
125
|
+
`Dashboard is already running (PID ${existing.pid})\n\n` +
|
|
126
|
+
` 🌐 ${chalk.cyan(`http://localhost${portStr}`)}\n\n` +
|
|
127
|
+
` Use ${chalk.yellow(`bgrun --stop ${dashboardName}`)} to stop it\n` +
|
|
128
|
+
` Use ${chalk.yellow(`bgrun --dashboard --force`)} to restart`,
|
|
129
|
+
'BGR Dashboard'
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Kill existing if force
|
|
135
|
+
if (existing) {
|
|
136
|
+
if (await isProcessRunning(existing.pid)) {
|
|
137
|
+
const detectedPorts = await getProcessPorts(existing.pid);
|
|
138
|
+
await terminateProcess(existing.pid);
|
|
139
|
+
for (const p of detectedPorts) {
|
|
140
|
+
await killProcessOnPort(p);
|
|
141
|
+
await waitForPortFree(p, 5000);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await retryDatabaseOperation(() => removeProcessByName(dashboardName));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Spawn the dashboard server as a managed process
|
|
148
|
+
// Port is NOT passed as CLI arg — Melina will auto-detect.
|
|
149
|
+
// If user wants a specific port, we pass it via BUN_PORT env var.
|
|
150
|
+
const { resolve } = require('path');
|
|
151
|
+
const scriptPath = resolve(process.argv[1]);
|
|
152
|
+
const spawnCommand = `bun run ${scriptPath} --_serve`;
|
|
153
|
+
const command = `bgrun --_serve`;
|
|
154
|
+
const stdoutPath = join(bgrDir, `${dashboardName}-out.txt`);
|
|
155
|
+
const stderrPath = join(bgrDir, `${dashboardName}-err.txt`);
|
|
156
|
+
|
|
157
|
+
await Bun.write(stdoutPath, '');
|
|
158
|
+
await Bun.write(stderrPath, '');
|
|
159
|
+
|
|
160
|
+
// Pass BUN_PORT env var only if user explicitly requested a port
|
|
161
|
+
const spawnEnv = { ...Bun.env };
|
|
162
|
+
if (requestedPort) {
|
|
163
|
+
spawnEnv.BUN_PORT = requestedPort;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const newProcess = Bun.spawn(getShellCommand(spawnCommand), {
|
|
167
|
+
env: spawnEnv,
|
|
168
|
+
cwd: bgrDir,
|
|
169
|
+
stdout: Bun.file(stdoutPath),
|
|
170
|
+
stderr: Bun.file(stderrPath),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
newProcess.unref();
|
|
174
|
+
|
|
175
|
+
// Resolve the actual child PID by traversing the process tree
|
|
176
|
+
// (cmd.exe → bun.exe), then detect which port it bound
|
|
177
|
+
await sleep(2000); // Give the server time to start and bind a port
|
|
178
|
+
const actualPid = await findChildPid(newProcess.pid);
|
|
179
|
+
|
|
180
|
+
// Detect the port the server actually bound to
|
|
181
|
+
let actualPort: number | null = null;
|
|
182
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
183
|
+
const ports = await getProcessPorts(actualPid);
|
|
184
|
+
if (ports.length > 0) {
|
|
185
|
+
actualPort = ports[0];
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
await sleep(1000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await retryDatabaseOperation(() =>
|
|
192
|
+
insertProcess({
|
|
193
|
+
pid: actualPid,
|
|
194
|
+
workdir: bgrDir,
|
|
195
|
+
command,
|
|
196
|
+
name: dashboardName,
|
|
197
|
+
env: '',
|
|
198
|
+
configPath: '',
|
|
199
|
+
stdout_path: stdoutPath,
|
|
200
|
+
stderr_path: stderrPath,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const portDisplay = actualPort ? String(actualPort) : '(detecting...)';
|
|
205
|
+
const urlDisplay = actualPort ? `http://localhost:${actualPort}` : 'http://localhost (port auto-assigned)';
|
|
206
|
+
|
|
207
|
+
const msg = dedent`
|
|
208
|
+
${chalk.bold('⚡ BGR Dashboard launched')}
|
|
209
|
+
${chalk.gray('─'.repeat(40))}
|
|
210
|
+
|
|
211
|
+
🌐 Open in browser: ${chalk.cyan.underline(urlDisplay)}
|
|
212
|
+
📊 Manage all your processes from the web UI
|
|
213
|
+
🔄 Auto-refreshes every 3 seconds
|
|
214
|
+
|
|
215
|
+
${chalk.gray('─'.repeat(40))}
|
|
216
|
+
Process: ${chalk.white(dashboardName)} | PID: ${chalk.white(String(actualPid))} | Port: ${chalk.white(portDisplay)}
|
|
217
|
+
|
|
218
|
+
${chalk.yellow('bgrun bgr-dashboard --logs')} View dashboard logs
|
|
219
|
+
${chalk.yellow('bgrun --stop bgr-dashboard')} Stop the dashboard
|
|
220
|
+
${chalk.yellow('bgrun --restart bgr-dashboard')} Restart the dashboard
|
|
221
|
+
`;
|
|
222
|
+
announce(msg, 'BGR Dashboard');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (values.version) {
|
|
227
|
+
console.log(`bgrun version: ${await getVersion()}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (values.help) {
|
|
232
|
+
await showHelp();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Commands flow
|
|
237
|
+
if (values.nuke) {
|
|
238
|
+
await handleDeleteAll();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (values.clean) {
|
|
243
|
+
await handleClean();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const name = (values.name as string) || positionals[0];
|
|
248
|
+
|
|
249
|
+
// Delete
|
|
250
|
+
if (values.delete) {
|
|
251
|
+
// bgr --delete (bool)
|
|
252
|
+
if (name) {
|
|
253
|
+
await handleDelete(name);
|
|
254
|
+
} else {
|
|
255
|
+
error("Please specify a process name to delete.");
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Restart
|
|
261
|
+
if (values.restart) {
|
|
262
|
+
if (!name) {
|
|
263
|
+
error("Please specify a process name to restart.");
|
|
264
|
+
}
|
|
265
|
+
await handleRun({
|
|
266
|
+
action: 'run',
|
|
267
|
+
name: name,
|
|
268
|
+
force: true,
|
|
269
|
+
// other options undefined, handleRun will look up process
|
|
270
|
+
remoteName: '',
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Stop
|
|
276
|
+
if (values.stop) {
|
|
277
|
+
if (!name) {
|
|
278
|
+
error("Please specify a process name to stop.");
|
|
279
|
+
}
|
|
280
|
+
await handleStop(name);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Logs
|
|
285
|
+
if (values.logs || values["log-stdout"] || values["log-stderr"]) {
|
|
286
|
+
if (!name) {
|
|
287
|
+
error("Please specify a process name to show logs for.");
|
|
288
|
+
}
|
|
289
|
+
const logType = values["log-stdout"] ? 'stdout' : (values["log-stderr"] ? 'stderr' : 'both');
|
|
290
|
+
const lines = values.lines ? parseInt(values.lines as string) : undefined;
|
|
291
|
+
await showLogs(name, logType, lines);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Watch
|
|
296
|
+
if (values.watch) {
|
|
297
|
+
await handleWatch({
|
|
298
|
+
action: 'watch',
|
|
299
|
+
name: name,
|
|
300
|
+
command: values.command as string | undefined,
|
|
301
|
+
directory: values.directory as string | undefined,
|
|
302
|
+
configPath: values.config as string | undefined,
|
|
303
|
+
force: values.force as boolean | undefined,
|
|
304
|
+
remoteName: '',
|
|
305
|
+
dbPath: values.db as string | undefined,
|
|
306
|
+
stdout: values.stdout as string | undefined,
|
|
307
|
+
stderr: values.stderr as string | undefined
|
|
308
|
+
}, {
|
|
309
|
+
showLogs: (values.logs as boolean) || false,
|
|
310
|
+
logType: 'both',
|
|
311
|
+
lines: values.lines ? parseInt(values.lines as string) : undefined
|
|
312
|
+
});
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// List or Run or Details
|
|
317
|
+
if (name) {
|
|
318
|
+
if (!values.command && !values.directory) {
|
|
319
|
+
await showDetails(name);
|
|
320
|
+
} else {
|
|
321
|
+
await handleRun({
|
|
322
|
+
action: 'run',
|
|
323
|
+
name: name,
|
|
324
|
+
command: values.command as string | undefined,
|
|
325
|
+
directory: values.directory as string | undefined,
|
|
326
|
+
configPath: values.config as string | undefined,
|
|
327
|
+
force: values.force as boolean | undefined,
|
|
328
|
+
fetch: values.fetch as boolean | undefined,
|
|
329
|
+
remoteName: '',
|
|
330
|
+
dbPath: values.db as string | undefined,
|
|
331
|
+
stdout: values.stdout as string | undefined,
|
|
332
|
+
stderr: values.stderr as string | undefined
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
if (values.command) {
|
|
337
|
+
error("Process name is required.");
|
|
338
|
+
}
|
|
339
|
+
await showAll({
|
|
340
|
+
json: values.json as boolean | undefined,
|
|
341
|
+
filter: values.filter as string | undefined
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
run().catch(err => {
|
|
347
|
+
console.error(chalk.red(err));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import boxen from "boxen";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
export function announce(message: string, title?: string) {
|
|
5
|
+
console.log(
|
|
6
|
+
boxen(chalk.white(message), {
|
|
7
|
+
padding: 1,
|
|
8
|
+
margin: 1,
|
|
9
|
+
borderColor: 'green',
|
|
10
|
+
title: title || "bgrun",
|
|
11
|
+
titleAlignment: 'center',
|
|
12
|
+
borderStyle: 'round'
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function error(message: string) {
|
|
18
|
+
console.error(
|
|
19
|
+
boxen(chalk.red(message), {
|
|
20
|
+
padding: 1,
|
|
21
|
+
margin: 1,
|
|
22
|
+
borderColor: 'red',
|
|
23
|
+
title: "Error",
|
|
24
|
+
titleAlignment: 'center',
|
|
25
|
+
borderStyle: 'double'
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|