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/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
+ }