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.
@@ -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
+