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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bgr-startup.sh - Example startup script for BGR processes
|
|
3
|
+
# Place in /usr/local/bin/bgr-startup.sh and make executable
|
|
4
|
+
|
|
5
|
+
# Wait for network to be ready
|
|
6
|
+
sleep 5
|
|
7
|
+
|
|
8
|
+
# Start database services
|
|
9
|
+
echo "Starting database services..."
|
|
10
|
+
bgr --name postgres --command "postgres -D /var/lib/postgresql/data"
|
|
11
|
+
bgr --name redis --command "redis-server /etc/redis/redis.conf"
|
|
12
|
+
|
|
13
|
+
# Wait for databases to be ready
|
|
14
|
+
sleep 3
|
|
15
|
+
|
|
16
|
+
# Start web server / reverse proxy
|
|
17
|
+
echo "Starting web server..."
|
|
18
|
+
bgr --name caddy --directory /etc/caddy --command "caddy run"
|
|
19
|
+
|
|
20
|
+
# Start application services
|
|
21
|
+
echo "Starting application services..."
|
|
22
|
+
bgr --name api \
|
|
23
|
+
--directory /var/www/api \
|
|
24
|
+
--command "node dist/server.js" \
|
|
25
|
+
--config production.toml
|
|
26
|
+
|
|
27
|
+
bgr --name worker \
|
|
28
|
+
--directory /var/www/worker \
|
|
29
|
+
--command "python worker.py" \
|
|
30
|
+
--config production.toml
|
|
31
|
+
|
|
32
|
+
# Optional: Start monitoring guards
|
|
33
|
+
echo "Starting process guards..."
|
|
34
|
+
bgr --name api-guard \
|
|
35
|
+
--directory /var/www/api \
|
|
36
|
+
--command "bun /opt/bgr/examples/guard.ts api 30"
|
|
37
|
+
|
|
38
|
+
# Show final status
|
|
39
|
+
echo "All services started. Current status:"
|
|
40
|
+
bgr
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bgrun",
|
|
3
|
+
"version": "3.3.1",
|
|
4
|
+
"description": "bgrun — A lightweight process manager for Bun",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/api.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/api.ts"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"bgrun": "./dist/index.js",
|
|
12
|
+
"bgr": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun run ./src/build.ts",
|
|
16
|
+
"test": "bun test",
|
|
17
|
+
"prepublishOnly": "bun run build"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"dashboard/app",
|
|
23
|
+
"README.md",
|
|
24
|
+
"examples/bgr-startup.sh"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"process-manager",
|
|
28
|
+
"bun",
|
|
29
|
+
"monitoring",
|
|
30
|
+
"devops",
|
|
31
|
+
"deployment",
|
|
32
|
+
"background",
|
|
33
|
+
"daemon"
|
|
34
|
+
],
|
|
35
|
+
"author": "7flash",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/Mements/bgr.git"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"bun-types": "latest"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"typescript": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"sqlite-zod-orm": "latest",
|
|
49
|
+
"@ments/web": "^1.12.1",
|
|
50
|
+
"boxen": "^8.0.1",
|
|
51
|
+
"chalk": "^5.4.1",
|
|
52
|
+
"dedent": "^1.5.3",
|
|
53
|
+
"melina": "^1.3.2",
|
|
54
|
+
"react": "^19.2.4",
|
|
55
|
+
"react-dom": "^19.2.4"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"bun": ">=1.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BGR Public API (package: bgrun)
|
|
3
|
+
*
|
|
4
|
+
* Import from 'bgrun' to use these functions in your own process-managing apps.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { getAllProcesses, isProcessRunning, handleRun } from 'bgrun'
|
|
9
|
+
*
|
|
10
|
+
* // List all managed processes
|
|
11
|
+
* const processes = getAllProcesses()
|
|
12
|
+
*
|
|
13
|
+
* // Check if a process is running
|
|
14
|
+
* const alive = await isProcessRunning(process.pid)
|
|
15
|
+
*
|
|
16
|
+
* // Start a new managed process
|
|
17
|
+
* await handleRun({ name: 'my-app', command: 'bun run dev', directory: './my-app', action: 'run', remoteName: '' })
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// --- Database Operations ---
|
|
22
|
+
export { db, getAllProcesses, getProcess, insertProcess, removeProcess, removeProcessByName, removeAllProcesses, retryDatabaseOperation } from './db'
|
|
23
|
+
|
|
24
|
+
// --- Process Operations ---
|
|
25
|
+
export { isProcessRunning, terminateProcess, readFileTail, getProcessPorts, findChildPid, findPidByPort, getShellCommand, killProcessOnPort, waitForPortFree, ensureDir, getHomeDir, isWindows } from './platform'
|
|
26
|
+
|
|
27
|
+
// --- High-Level Commands ---
|
|
28
|
+
export { handleRun } from './commands/run'
|
|
29
|
+
|
|
30
|
+
// --- Utilities ---
|
|
31
|
+
export { getVersion, calculateRuntime, parseEnvString, validateDirectory } from './utils'
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { EOL } from 'os';
|
|
2
|
+
|
|
3
|
+
console.log("Starting build process for bgrun...");
|
|
4
|
+
|
|
5
|
+
const result = await Bun.build({
|
|
6
|
+
entrypoints: ['./src/index.ts'],
|
|
7
|
+
outdir: './dist',
|
|
8
|
+
target: 'bun',
|
|
9
|
+
format: 'esm',
|
|
10
|
+
minify: false,
|
|
11
|
+
// Mark all packages as external to rely on node_modules
|
|
12
|
+
// This avoids bundling native modules or mismatched React versions
|
|
13
|
+
packages: "external",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
console.error("Build failed");
|
|
18
|
+
for (const message of result.logs) {
|
|
19
|
+
console.error(message);
|
|
20
|
+
}
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const outFile = result.outputs[0].path;
|
|
25
|
+
|
|
26
|
+
console.log(`Build successful! Executable created at: ${outFile}`);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
import { getProcess, removeProcessByName, removeProcess, getAllProcesses, removeAllProcesses } from "../db";
|
|
3
|
+
import { isProcessRunning, terminateProcess } from "../platform";
|
|
4
|
+
import { announce, error } from "../logger";
|
|
5
|
+
import type { ProcessRecord } from "../types";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
|
|
8
|
+
export async function handleDelete(name: string) {
|
|
9
|
+
const process = getProcess(name); // Wrapper or raw query needed if getProcess doesn't return full type
|
|
10
|
+
// getProcess returns ProcessRecord | null
|
|
11
|
+
|
|
12
|
+
if (!process) {
|
|
13
|
+
error(`No process found named '${name}'`);
|
|
14
|
+
// error calls process.exit(1), so we return but TS doesn't know.
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isRunning = await isProcessRunning(process.pid);
|
|
19
|
+
if (isRunning) {
|
|
20
|
+
await terminateProcess(process.pid);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (fs.existsSync(process.stdout_path)) {
|
|
24
|
+
try { fs.unlinkSync(process.stdout_path); } catch { }
|
|
25
|
+
}
|
|
26
|
+
if (fs.existsSync(process.stderr_path)) {
|
|
27
|
+
try { fs.unlinkSync(process.stderr_path); } catch { }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
removeProcessByName(name);
|
|
31
|
+
announce(`Process '${name}' has been ${isRunning ? 'stopped and ' : ''}deleted`, "Process Deleted");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleClean() {
|
|
35
|
+
const processes = getAllProcesses();
|
|
36
|
+
let cleanedCount = 0;
|
|
37
|
+
let deletedLogs = 0;
|
|
38
|
+
|
|
39
|
+
for (const proc of processes) {
|
|
40
|
+
const running = await isProcessRunning(proc.pid);
|
|
41
|
+
if (!running) {
|
|
42
|
+
removeProcess(proc.pid);
|
|
43
|
+
cleanedCount++;
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(proc.stdout_path)) {
|
|
46
|
+
try { fs.unlinkSync(proc.stdout_path); deletedLogs++; } catch { }
|
|
47
|
+
}
|
|
48
|
+
if (fs.existsSync(proc.stderr_path)) {
|
|
49
|
+
try { fs.unlinkSync(proc.stderr_path); deletedLogs++; } catch { }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (cleanedCount === 0) {
|
|
55
|
+
announce("No stopped processes found to clean.", "Clean Complete");
|
|
56
|
+
} else {
|
|
57
|
+
announce(
|
|
58
|
+
`Cleaned ${cleanedCount} stopped ${cleanedCount === 1 ? 'process' : 'processes'} and removed ${deletedLogs} log ${deletedLogs === 1 ? 'file' : 'files'}.`,
|
|
59
|
+
"Clean Complete"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function handleStop(name: string) {
|
|
65
|
+
const proc = getProcess(name);
|
|
66
|
+
|
|
67
|
+
if (!proc) {
|
|
68
|
+
error(`No process found named '${name}'`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isRunning = await isProcessRunning(proc.pid);
|
|
73
|
+
if (!isRunning) {
|
|
74
|
+
announce(`Process '${name}' is already stopped.`, "Process Stop");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Detect ports the process is using BEFORE killing it
|
|
79
|
+
const { getProcessPorts, killProcessOnPort } = await import("../platform");
|
|
80
|
+
const ports = await getProcessPorts(proc.pid);
|
|
81
|
+
|
|
82
|
+
await terminateProcess(proc.pid);
|
|
83
|
+
|
|
84
|
+
// Also kill by detected ports as safety net
|
|
85
|
+
for (const port of ports) {
|
|
86
|
+
await killProcessOnPort(port);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
announce(`Process '${name}' has been stopped (kept in registry).`, "Process Stopped");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function handleDeleteAll() {
|
|
93
|
+
const processes = getAllProcesses();
|
|
94
|
+
if (processes.length === 0) {
|
|
95
|
+
announce("There are no processes to delete.", "Delete All");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { getProcessPorts, killProcessOnPort, waitForPortFree } = await import("../platform");
|
|
100
|
+
let killedCount = 0;
|
|
101
|
+
let portsFreed = 0;
|
|
102
|
+
|
|
103
|
+
for (const proc of processes) {
|
|
104
|
+
const running = await isProcessRunning(proc.pid);
|
|
105
|
+
|
|
106
|
+
if (running) {
|
|
107
|
+
// Detect ports BEFORE killing so we can clean them up
|
|
108
|
+
const ports = await getProcessPorts(proc.pid);
|
|
109
|
+
|
|
110
|
+
// Force-kill the process tree
|
|
111
|
+
await terminateProcess(proc.pid, true);
|
|
112
|
+
killedCount++;
|
|
113
|
+
|
|
114
|
+
// Kill anything still holding the ports
|
|
115
|
+
for (const port of ports) {
|
|
116
|
+
await killProcessOnPort(port);
|
|
117
|
+
const freed = await waitForPortFree(port, 3000);
|
|
118
|
+
if (!freed) {
|
|
119
|
+
await killProcessOnPort(port);
|
|
120
|
+
await waitForPortFree(port, 2000);
|
|
121
|
+
}
|
|
122
|
+
portsFreed++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Clean up log files
|
|
127
|
+
if (fs.existsSync(proc.stdout_path)) {
|
|
128
|
+
try { fs.unlinkSync(proc.stdout_path); } catch { }
|
|
129
|
+
}
|
|
130
|
+
if (fs.existsSync(proc.stderr_path)) {
|
|
131
|
+
try { fs.unlinkSync(proc.stderr_path); } catch { }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
removeAllProcesses();
|
|
136
|
+
|
|
137
|
+
const parts = [`${processes.length} ${processes.length === 1 ? 'process' : 'processes'} deleted`];
|
|
138
|
+
if (killedCount > 0) parts.push(`${killedCount} force-killed`);
|
|
139
|
+
if (portsFreed > 0) parts.push(`${portsFreed} ${portsFreed === 1 ? 'port' : 'ports'} freed`);
|
|
140
|
+
|
|
141
|
+
announce(parts.join(', ') + '.', "Nuke Complete");
|
|
142
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
|
|
2
|
+
import { error, announce } from "../logger";
|
|
3
|
+
import { getProcess } from "../db";
|
|
4
|
+
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
|
5
|
+
import { getProcessPorts } from "../platform";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
export async function showDetails(name: string) {
|
|
9
|
+
const proc = getProcess(name);
|
|
10
|
+
if (!proc) {
|
|
11
|
+
error(`No process found named '${name}'`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
16
|
+
const runtime = calculateRuntime(proc.timestamp);
|
|
17
|
+
const envVars = parseEnvString(proc.env);
|
|
18
|
+
|
|
19
|
+
// Detect actual ports via OS
|
|
20
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
21
|
+
|
|
22
|
+
const portDisplay = ports.length > 0
|
|
23
|
+
? ports.map(p => chalk.hex('#FF6B6B')(`:${p}`)).join(', ')
|
|
24
|
+
: null;
|
|
25
|
+
|
|
26
|
+
const details = `
|
|
27
|
+
${chalk.bold('Process Details:')}
|
|
28
|
+
${chalk.gray('═'.repeat(50))}
|
|
29
|
+
${chalk.cyan.bold('Name:')} ${proc.name}
|
|
30
|
+
${chalk.yellow.bold('PID:')} ${proc.pid}${portDisplay ? `\n${chalk.hex('#FF6B6B').bold('Port:')} ${portDisplay}` : ''}
|
|
31
|
+
${chalk.bold('Status:')} ${isRunning ? chalk.green.bold("● Running") : chalk.red.bold("○ Stopped")}
|
|
32
|
+
${chalk.magenta.bold('Runtime:')} ${runtime}
|
|
33
|
+
${chalk.blue.bold('Working Directory:')} ${proc.workdir}
|
|
34
|
+
${chalk.white.bold('Command:')} ${proc.command}
|
|
35
|
+
${chalk.gray.bold('Config Path:')} ${proc.configPath}
|
|
36
|
+
${chalk.green.bold('Stdout Path:')} ${proc.stdout_path}
|
|
37
|
+
${chalk.red.bold('Stderr Path:')} ${proc.stderr_path}
|
|
38
|
+
|
|
39
|
+
${chalk.bold('🔧 Environment Variables:')}
|
|
40
|
+
${chalk.gray('═'.repeat(50))}
|
|
41
|
+
${Object.entries(envVars)
|
|
42
|
+
.map(([key, value]) => `${chalk.cyan.bold(key)} = ${chalk.yellow(value)}`)
|
|
43
|
+
.join('\n')}
|
|
44
|
+
`;
|
|
45
|
+
announce(details, `Process Details: ${name}`);
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ProcessTableRow, renderProcessTable } from "../table";
|
|
3
|
+
import { ProcessRecord } from "../types";
|
|
4
|
+
import { getAllProcesses } from "../db";
|
|
5
|
+
import { announce } from "../logger";
|
|
6
|
+
import { isProcessRunning, calculateRuntime, parseEnvString } from "../utils";
|
|
7
|
+
import { getProcessPorts } from "../platform";
|
|
8
|
+
|
|
9
|
+
export async function showAll(opts?: { json?: boolean; filter?: string }) {
|
|
10
|
+
const processes = getAllProcesses();
|
|
11
|
+
|
|
12
|
+
// Apply filter by env.BGR_GROUP if provided
|
|
13
|
+
const filtered = processes.filter((proc) => {
|
|
14
|
+
if (!opts?.filter) return true;
|
|
15
|
+
const envVars = parseEnvString(proc.env);
|
|
16
|
+
return envVars["BGR_GROUP"] === opts.filter;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (opts?.json) {
|
|
20
|
+
// JSON output with filtered env variables
|
|
21
|
+
const jsonData: any[] = [];
|
|
22
|
+
|
|
23
|
+
for (const proc of filtered) {
|
|
24
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
25
|
+
const envVars = parseEnvString(proc.env);
|
|
26
|
+
|
|
27
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
28
|
+
jsonData.push({
|
|
29
|
+
pid: proc.pid,
|
|
30
|
+
name: proc.name,
|
|
31
|
+
ports: ports.length > 0 ? ports : undefined,
|
|
32
|
+
status: isRunning ? "running" : "stopped",
|
|
33
|
+
env: envVars,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(JSON.stringify(jsonData, null, 2));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Table output
|
|
42
|
+
const tableData: ProcessTableRow[] = [];
|
|
43
|
+
|
|
44
|
+
for (const proc of filtered) {
|
|
45
|
+
const isRunning = await isProcessRunning(proc.pid, proc.command);
|
|
46
|
+
const runtime = calculateRuntime(proc.timestamp);
|
|
47
|
+
|
|
48
|
+
const ports = isRunning ? await getProcessPorts(proc.pid) : [];
|
|
49
|
+
tableData.push({
|
|
50
|
+
id: proc.id,
|
|
51
|
+
pid: proc.pid,
|
|
52
|
+
name: proc.name,
|
|
53
|
+
port: ports.length > 0 ? ports.map(p => `:${p}`).join(',') : '-',
|
|
54
|
+
command: proc.command,
|
|
55
|
+
workdir: proc.workdir,
|
|
56
|
+
status: isRunning
|
|
57
|
+
? chalk.green.bold("● Running")
|
|
58
|
+
: chalk.red.bold("○ Stopped"),
|
|
59
|
+
runtime: runtime,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (tableData.length === 0) {
|
|
64
|
+
if (opts?.filter) {
|
|
65
|
+
announce(`No processes matched filter BGR_GROUP='${opts.filter}'.`, "No Matches");
|
|
66
|
+
} else {
|
|
67
|
+
announce("No processes found.", "Empty");
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tableOutput = renderProcessTable(tableData, {
|
|
73
|
+
padding: 1,
|
|
74
|
+
borderStyle: "rounded",
|
|
75
|
+
showHeaders: true,
|
|
76
|
+
});
|
|
77
|
+
console.log(tableOutput);
|
|
78
|
+
|
|
79
|
+
const runningCount = tableData.filter((p) => p.status.includes("Running")).length;
|
|
80
|
+
const stoppedCount = tableData.filter((p) => p.status.includes("Stopped")).length;
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.cyan(
|
|
83
|
+
`Total: ${tableData.length} processes (${chalk.green(`${runningCount} running`)}, ${chalk.red(`${stoppedCount} stopped`)})`
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getProcess } from "../db";
|
|
2
|
+
import { error } from "../logger";
|
|
3
|
+
import { readFileTail } from "../platform";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
|
|
7
|
+
export async function showLogs(name: string, logType: 'stdout' | 'stderr' | 'both' = 'both', lines?: number) {
|
|
8
|
+
const proc = getProcess(name);
|
|
9
|
+
if (!proc) {
|
|
10
|
+
error(`No process found named '${name}'`);
|
|
11
|
+
return; // TS shut up
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (logType === 'both' || logType === 'stdout') {
|
|
15
|
+
console.log(chalk.green.bold(`📄 Stdout logs for ${name}:`));
|
|
16
|
+
console.log(chalk.gray('═'.repeat(50)));
|
|
17
|
+
|
|
18
|
+
if (fs.existsSync(proc.stdout_path)) {
|
|
19
|
+
try {
|
|
20
|
+
const output = await readFileTail(proc.stdout_path, lines);
|
|
21
|
+
console.log(output || chalk.gray('(no output)'));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.log(chalk.red(`Error reading stdout: ${err}`));
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
console.log(chalk.gray('(log file not found)'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (logType === 'both') {
|
|
30
|
+
console.log('\n');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (logType === 'both' || logType === 'stderr') {
|
|
35
|
+
console.log(chalk.red.bold(`📄 Stderr logs for ${name}:`));
|
|
36
|
+
console.log(chalk.gray('═'.repeat(50)));
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(proc.stderr_path)) {
|
|
39
|
+
try {
|
|
40
|
+
const output = await readFileTail(proc.stderr_path, lines);
|
|
41
|
+
console.log(output || chalk.gray('(no errors)'));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(chalk.red(`Error reading stderr: ${err}`));
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
console.log(chalk.gray('(log file not found)'));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { CommandOptions } from "../types";
|
|
2
|
+
import { getProcess, removeProcessByName, retryDatabaseOperation, insertProcess } from "../db";
|
|
3
|
+
import { isProcessRunning, terminateProcess, getHomeDir, getShellCommand, killProcessOnPort, findChildPid, getProcessPorts, waitForPortFree } from "../platform";
|
|
4
|
+
import { error, announce } from "../logger";
|
|
5
|
+
import { validateDirectory, parseEnvString } from "../utils";
|
|
6
|
+
import { parseConfigFile } from "../config";
|
|
7
|
+
import { $ } from "bun";
|
|
8
|
+
import { sleep } from "bun";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
|
|
11
|
+
const homePath = getHomeDir();
|
|
12
|
+
|
|
13
|
+
export async function handleRun(options: CommandOptions) {
|
|
14
|
+
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
15
|
+
|
|
16
|
+
const existingProcess = name ? getProcess(name) : null;
|
|
17
|
+
|
|
18
|
+
if (existingProcess) {
|
|
19
|
+
const finalDirectory = directory || existingProcess.workdir;
|
|
20
|
+
validateDirectory(finalDirectory);
|
|
21
|
+
$.cwd(finalDirectory);
|
|
22
|
+
|
|
23
|
+
if (fetch) {
|
|
24
|
+
if (!require('fs').existsSync(require('path').join(finalDirectory, '.git'))) {
|
|
25
|
+
error(`Cannot --fetch: '${finalDirectory}' is not a Git repository.`);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await $`git fetch origin`;
|
|
29
|
+
const localHash = (await $`git rev-parse HEAD`.text()).trim();
|
|
30
|
+
const remoteHash = (await $`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
31
|
+
|
|
32
|
+
if (localHash !== remoteHash) {
|
|
33
|
+
await $`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
34
|
+
announce("📥 Pulled latest changes", "Git Update");
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
error(`Failed to pull latest changes: ${err}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
42
|
+
if (isRunning && !force) {
|
|
43
|
+
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Detect ports BEFORE killing so we can clean them up
|
|
47
|
+
let detectedPorts: number[] = [];
|
|
48
|
+
if (isRunning) {
|
|
49
|
+
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isRunning) {
|
|
53
|
+
await terminateProcess(existingProcess.pid);
|
|
54
|
+
announce(`🔥 Terminated existing process '${name}'`, "Process Terminated");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Kill anything still on the ports the old process was using
|
|
58
|
+
for (const port of detectedPorts) {
|
|
59
|
+
await killProcessOnPort(port);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Wait for all detected ports to free up
|
|
63
|
+
for (const port of detectedPorts) {
|
|
64
|
+
const freed = await waitForPortFree(port, 5000);
|
|
65
|
+
if (!freed) {
|
|
66
|
+
// Retry kill and wait once more
|
|
67
|
+
await killProcessOnPort(port);
|
|
68
|
+
await waitForPortFree(port, 3000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await retryDatabaseOperation(() =>
|
|
73
|
+
removeProcessByName(name!)
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
if (!directory || !name || !command) {
|
|
77
|
+
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
78
|
+
}
|
|
79
|
+
validateDirectory(directory!);
|
|
80
|
+
$.cwd(directory!);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const finalCommand = command || existingProcess!.command;
|
|
84
|
+
const finalDirectory = directory || (existingProcess?.workdir!);
|
|
85
|
+
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
86
|
+
|
|
87
|
+
let finalConfigPath: string | undefined | null;
|
|
88
|
+
if (configPath !== undefined) {
|
|
89
|
+
finalConfigPath = configPath;
|
|
90
|
+
} else if (existingProcess) {
|
|
91
|
+
finalConfigPath = existingProcess.configPath;
|
|
92
|
+
} else {
|
|
93
|
+
finalConfigPath = '.config.toml';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (finalConfigPath) {
|
|
97
|
+
const fullConfigPath = join(finalDirectory, finalConfigPath);
|
|
98
|
+
|
|
99
|
+
if (await Bun.file(fullConfigPath).exists()) {
|
|
100
|
+
try {
|
|
101
|
+
const newConfigEnv = await parseConfigFile(fullConfigPath);
|
|
102
|
+
finalEnv = { ...finalEnv, ...newConfigEnv };
|
|
103
|
+
console.log(`Loaded config from ${finalConfigPath}`);
|
|
104
|
+
} catch (err: any) {
|
|
105
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join(homePath, ".bgr", `${name}-out.txt`);
|
|
113
|
+
Bun.write(stdoutPath, '');
|
|
114
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join(homePath, ".bgr", `${name}-err.txt`);
|
|
115
|
+
Bun.write(stderrPath, '');
|
|
116
|
+
|
|
117
|
+
const newProcess = Bun.spawn(getShellCommand(finalCommand!), {
|
|
118
|
+
env: { ...Bun.env, ...finalEnv },
|
|
119
|
+
cwd: finalDirectory,
|
|
120
|
+
stdout: Bun.file(stdoutPath),
|
|
121
|
+
stderr: Bun.file(stderrPath),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
newProcess.unref();
|
|
125
|
+
// Give shell a moment to spawn child, then find PID before shell exits
|
|
126
|
+
await sleep(100);
|
|
127
|
+
// Find the actual child PID (shell wrapper exits immediately after spawning)
|
|
128
|
+
const actualPid = await findChildPid(newProcess.pid);
|
|
129
|
+
// Wait more for subprocess to initialize
|
|
130
|
+
await sleep(400);
|
|
131
|
+
|
|
132
|
+
await retryDatabaseOperation(() =>
|
|
133
|
+
insertProcess({
|
|
134
|
+
pid: actualPid,
|
|
135
|
+
workdir: finalDirectory,
|
|
136
|
+
command: finalCommand!,
|
|
137
|
+
name: name!,
|
|
138
|
+
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
139
|
+
configPath: finalConfigPath || '',
|
|
140
|
+
stdout_path: stdoutPath,
|
|
141
|
+
stderr_path: stderrPath,
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
announce(
|
|
146
|
+
`${existingProcess ? '🔄 Restarted' : '🚀 Launched'} process "${name}" with PID ${actualPid}`,
|
|
147
|
+
"Process Started"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|