bgrun 3.12.11 → 3.12.13
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 +2 -2
- package/dashboard/app/api/config/[name]/route.ts +1 -1
- package/dashboard/app/api/debug/route.ts +1 -1
- package/dashboard/app/api/dependencies/route.ts +40 -40
- package/dashboard/app/api/deploy/[name]/route.ts +1 -1
- package/dashboard/app/api/deploy-all/route.ts +25 -25
- package/dashboard/app/api/deps/route.ts +3 -3
- package/dashboard/app/api/guard/route.ts +1 -1
- package/dashboard/app/api/guard-all/route.ts +1 -1
- package/dashboard/app/api/guard-events/route.ts +4 -4
- package/dashboard/app/api/history/route.ts +105 -105
- package/dashboard/app/api/logs/[name]/route.ts +100 -100
- package/dashboard/app/api/logs/rotate/route.ts +2 -2
- package/dashboard/app/api/next-port/route.ts +32 -32
- package/dashboard/app/api/processes/[name]/route.ts +2 -2
- package/dashboard/app/api/processes/route.ts +4 -4
- package/dashboard/app/api/restart/[name]/route.ts +2 -2
- package/dashboard/app/api/start/route.ts +2 -2
- package/dashboard/app/api/stop/[name]/route.ts +2 -2
- package/dashboard/app/api/templates/route.ts +46 -46
- package/dashboard/app/api/version/route.ts +1 -1
- package/dashboard/lib/runtime.ts +49 -0
- package/dist/api.js +94 -67
- package/dist/deploy.js +1373 -0
- package/dist/deps.js +1004 -0
- package/dist/index.js +224 -224
- package/dist/log-rotation.js +95 -0
- package/dist/server.js +1488 -0
- package/package.json +2 -17
- package/src/api.ts +0 -63
- package/src/build.ts +0 -24
- package/src/commands/cleanup.ts +0 -141
- package/src/commands/details.ts +0 -60
- package/src/commands/list.ts +0 -133
- package/src/commands/logs.ts +0 -49
- package/src/commands/run.ts +0 -217
- package/src/commands/watch.ts +0 -223
- package/src/config.ts +0 -37
- package/src/db.ts +0 -422
- package/src/deploy.ts +0 -163
- package/src/deps.ts +0 -126
- package/src/guard.ts +0 -208
- package/src/index.ts +0 -623
- package/src/log-rotation.ts +0 -93
- package/src/logger.ts +0 -40
- package/src/platform.ts +0 -665
- package/src/server.ts +0 -217
- package/src/table.ts +0 -232
- package/src/types.ts +0 -14
- package/src/utils.ts +0 -96
package/dist/deploy.js
ADDED
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = import.meta.require;
|
|
18
|
+
|
|
19
|
+
// src/platform.ts
|
|
20
|
+
var exports_platform = {};
|
|
21
|
+
__export(exports_platform, {
|
|
22
|
+
waitForPortFree: () => waitForPortFree,
|
|
23
|
+
terminateProcess: () => terminateProcess,
|
|
24
|
+
reconcileProcessPids: () => reconcileProcessPids,
|
|
25
|
+
readFileTail: () => readFileTail,
|
|
26
|
+
psExec: () => psExec,
|
|
27
|
+
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
28
|
+
killProcessOnPort: () => killProcessOnPort,
|
|
29
|
+
isWindows: () => isWindows,
|
|
30
|
+
isProcessRunning: () => isProcessRunning,
|
|
31
|
+
isPortFree: () => isPortFree,
|
|
32
|
+
getShellCommand: () => getShellCommand,
|
|
33
|
+
getProcessPorts: () => getProcessPorts,
|
|
34
|
+
getProcessMemory: () => getProcessMemory,
|
|
35
|
+
getProcessBatchResources: () => getProcessBatchResources,
|
|
36
|
+
getPortInfo: () => getPortInfo,
|
|
37
|
+
getHomeDir: () => getHomeDir,
|
|
38
|
+
findPidByPort: () => findPidByPort,
|
|
39
|
+
findChildPid: () => findChildPid,
|
|
40
|
+
ensureDir: () => ensureDir,
|
|
41
|
+
copyFile: () => copyFile
|
|
42
|
+
});
|
|
43
|
+
import * as fs from "fs";
|
|
44
|
+
import * as os from "os";
|
|
45
|
+
import { join } from "path";
|
|
46
|
+
var {$ } = globalThis.Bun;
|
|
47
|
+
import { createMeasure } from "measure-fn";
|
|
48
|
+
function psExec(command, _timeoutMs = 3000) {
|
|
49
|
+
const tmpFile = join(os.tmpdir(), `bgr-ps-${Date.now()}.ps1`);
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(tmpFile, command);
|
|
52
|
+
const result = Bun.spawnSync(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile]);
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(tmpFile);
|
|
55
|
+
} catch {}
|
|
56
|
+
return result.stdout?.toString() || "";
|
|
57
|
+
} catch {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(tmpFile);
|
|
60
|
+
} catch {}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isWindows() {
|
|
65
|
+
return process.platform === "win32";
|
|
66
|
+
}
|
|
67
|
+
function getHomeDir() {
|
|
68
|
+
return os.homedir();
|
|
69
|
+
}
|
|
70
|
+
async function isProcessRunning(pid, command) {
|
|
71
|
+
if (pid <= 0)
|
|
72
|
+
return false;
|
|
73
|
+
return await plat.measure(`PID ${pid} alive?`, async () => {
|
|
74
|
+
try {
|
|
75
|
+
if (command && (command.includes("docker run") || command.includes("docker-compose up") || command.includes("docker compose up"))) {
|
|
76
|
+
return await isDockerContainerRunning(command);
|
|
77
|
+
}
|
|
78
|
+
if (isWindows()) {
|
|
79
|
+
try {
|
|
80
|
+
process.kill(pid, 0);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
|
|
84
|
+
return output === String(pid);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
88
|
+
return result.includes(`${pid}`);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}) ?? false;
|
|
94
|
+
}
|
|
95
|
+
async function isDockerContainerRunning(command) {
|
|
96
|
+
try {
|
|
97
|
+
const nameMatch = command.match(/--name\s+["']?(\S+?)["']?(?:\s|$)/);
|
|
98
|
+
if (nameMatch) {
|
|
99
|
+
const containerName = nameMatch[1];
|
|
100
|
+
const result = await $`docker inspect -f "{{.State.Running}}" ${containerName}`.nothrow().text();
|
|
101
|
+
return result.trim() === "true";
|
|
102
|
+
}
|
|
103
|
+
const imageMatch = command.match(/docker\s+run\s+.*?(?:-d\s+)?(\S+)\s*$/);
|
|
104
|
+
if (imageMatch) {
|
|
105
|
+
const imageName = imageMatch[1];
|
|
106
|
+
const result = await $`docker ps --filter ancestor=${imageName} --format "{{.ID}}"`.nothrow().text();
|
|
107
|
+
return result.trim().length > 0;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function getChildPids(pid) {
|
|
115
|
+
try {
|
|
116
|
+
if (isWindows()) {
|
|
117
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${pid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
118
|
+
return result.split(`
|
|
119
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
120
|
+
} else {
|
|
121
|
+
const result = await $`ps --no-headers -o pid --ppid ${pid}`.nothrow().text();
|
|
122
|
+
return result.trim().split(`
|
|
123
|
+
`).filter((p) => p.trim()).map((p) => parseInt(p)).filter((n) => !isNaN(n));
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function terminateProcess(pid, force = false) {
|
|
130
|
+
await plat.measure(`Terminate PID ${pid}`, async (m) => {
|
|
131
|
+
try {
|
|
132
|
+
if (isWindows()) {
|
|
133
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
134
|
+
} else {
|
|
135
|
+
const children = await m("Get children", () => getChildPids(pid)) ?? [];
|
|
136
|
+
const signal = force ? "KILL" : "TERM";
|
|
137
|
+
for (const childPid of children) {
|
|
138
|
+
try {
|
|
139
|
+
await $`kill -${signal} ${childPid}`.nothrow();
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
await Bun.sleep(500);
|
|
143
|
+
if (await isProcessRunning(pid)) {
|
|
144
|
+
await $`kill -${signal} ${pid}`.nothrow();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch {}
|
|
148
|
+
await Bun.sleep(300);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function isPortFree(port) {
|
|
152
|
+
try {
|
|
153
|
+
if (isWindows()) {
|
|
154
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
155
|
+
for (const line of result.split(`
|
|
156
|
+
`)) {
|
|
157
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
158
|
+
if (match) {
|
|
159
|
+
const pid = parseInt(match[2]);
|
|
160
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
} else {
|
|
167
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
168
|
+
const lines = result.trim().split(`
|
|
169
|
+
`).filter((l) => l.trim());
|
|
170
|
+
return lines.length <= 1;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function getPortInfo(port) {
|
|
177
|
+
try {
|
|
178
|
+
if (isWindows()) {
|
|
179
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
180
|
+
for (const line of result.split(`
|
|
181
|
+
`)) {
|
|
182
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*LISTENING\\s+(\\d+)`));
|
|
183
|
+
if (match) {
|
|
184
|
+
const pid = parseInt(match[2]);
|
|
185
|
+
if (pid > 0 && await isProcessRunning(pid)) {
|
|
186
|
+
const nameResult = await $`powershell -NoProfile -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`.nothrow().quiet().text();
|
|
187
|
+
return { inUse: true, pid, processName: nameResult.trim() || "unknown" };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { inUse: false };
|
|
192
|
+
} else {
|
|
193
|
+
const result = await $`ss -tln sport = :${port}`.nothrow().quiet().text();
|
|
194
|
+
const lines = result.trim().split(`
|
|
195
|
+
`).filter((l) => l.trim());
|
|
196
|
+
if (lines.length > 1) {
|
|
197
|
+
return { inUse: true };
|
|
198
|
+
}
|
|
199
|
+
return { inUse: false };
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
return { inUse: false };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function waitForPortFree(port, timeoutMs = 5000) {
|
|
206
|
+
const startTime = Date.now();
|
|
207
|
+
const pollInterval = 300;
|
|
208
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
209
|
+
if (await isPortFree(port)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
await Bun.sleep(pollInterval);
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
async function killProcessOnPort(port) {
|
|
217
|
+
try {
|
|
218
|
+
if (isWindows()) {
|
|
219
|
+
const result = await $`netstat -ano | findstr :${port}`.nothrow().quiet().text();
|
|
220
|
+
const pids = new Set;
|
|
221
|
+
for (const line of result.split(`
|
|
222
|
+
`)) {
|
|
223
|
+
const match = line.match(new RegExp(`:(${port})\\s+.*?\\s+(\\d+)\\s*$`));
|
|
224
|
+
if (match && parseInt(match[1]) === port) {
|
|
225
|
+
const pid = parseInt(match[2]);
|
|
226
|
+
if (pid > 0)
|
|
227
|
+
pids.add(pid);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const pid of pids) {
|
|
231
|
+
const alive = await isProcessRunning(pid);
|
|
232
|
+
if (alive) {
|
|
233
|
+
await $`taskkill /F /T /PID ${pid}`.nothrow().quiet();
|
|
234
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
235
|
+
} else {
|
|
236
|
+
console.warn(`\u26A0 Port ${port} held by zombie PID ${pid} (process dead, socket stuck in kernel). Will clear on reboot or TCP timeout.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const result = await $`lsof -ti :${port}`.nothrow().text();
|
|
241
|
+
if (result.trim()) {
|
|
242
|
+
const pids = result.trim().split(`
|
|
243
|
+
`).filter((pid) => pid);
|
|
244
|
+
for (const pid of pids) {
|
|
245
|
+
await $`kill -9 ${pid}`.nothrow();
|
|
246
|
+
console.log(`Killed process ${pid} using port ${port}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.warn(`Warning: Could not check or kill process on port ${port}: ${error}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function ensureDir(dirPath) {
|
|
255
|
+
if (!fs.existsSync(dirPath)) {
|
|
256
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function getShellCommand(command) {
|
|
260
|
+
if (isWindows()) {
|
|
261
|
+
return ["cmd", "/c", command];
|
|
262
|
+
} else {
|
|
263
|
+
return ["sh", "-c", command];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function findChildPid(parentPid) {
|
|
267
|
+
let currentPid = parentPid;
|
|
268
|
+
const maxDepth = 2;
|
|
269
|
+
for (let depth = 0;depth < maxDepth; depth++) {
|
|
270
|
+
try {
|
|
271
|
+
let childPids = [];
|
|
272
|
+
if (isWindows()) {
|
|
273
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter 'ParentProcessId=${currentPid}' | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
274
|
+
childPids = result.split(`
|
|
275
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
276
|
+
} else {
|
|
277
|
+
const result = await $`ps --no-headers -o pid --ppid ${currentPid}`.nothrow().text();
|
|
278
|
+
childPids = result.trim().split(`
|
|
279
|
+
`).map((line) => parseInt(line.trim())).filter((n) => !isNaN(n) && n > 0);
|
|
280
|
+
}
|
|
281
|
+
if (childPids.length === 0)
|
|
282
|
+
break;
|
|
283
|
+
currentPid = childPids[0];
|
|
284
|
+
} catch {
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return currentPid;
|
|
289
|
+
}
|
|
290
|
+
async function reconcileProcessPids(processes, deadPids) {
|
|
291
|
+
return await plat.measure("Reconcile PIDs", async () => {
|
|
292
|
+
const result = new Map;
|
|
293
|
+
const needsReconciliation = processes.filter((p) => deadPids.has(p.pid) && p.pid > 0);
|
|
294
|
+
if (needsReconciliation.length === 0)
|
|
295
|
+
return result;
|
|
296
|
+
try {
|
|
297
|
+
let runningProcs = [];
|
|
298
|
+
if (isWindows()) {
|
|
299
|
+
const output = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.CommandLine)" }`, 5000);
|
|
300
|
+
for (const line of output.split(`
|
|
301
|
+
`)) {
|
|
302
|
+
const sepIdx = line.indexOf("|");
|
|
303
|
+
if (sepIdx === -1)
|
|
304
|
+
continue;
|
|
305
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
306
|
+
const cmdLine = line.substring(sepIdx + 1).trim();
|
|
307
|
+
if (!isNaN(pid) && pid > 0 && cmdLine) {
|
|
308
|
+
runningProcs.push({ pid, cmdLine });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
const psOutput = await $`ps -eo pid,args --no-headers`.nothrow().quiet().text();
|
|
313
|
+
for (const line of psOutput.trim().split(`
|
|
314
|
+
`)) {
|
|
315
|
+
const match = line.trim().match(/^(\d+)\s+(.+)/);
|
|
316
|
+
if (match) {
|
|
317
|
+
runningProcs.push({ pid: parseInt(match[1]), cmdLine: match[2] });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const proc of needsReconciliation) {
|
|
322
|
+
const cmdParts = proc.command.split(/\s+/);
|
|
323
|
+
const workdirParts = proc.workdir.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
324
|
+
const workdirLast = workdirParts[workdirParts.length - 1]?.toLowerCase() || "";
|
|
325
|
+
let bestMatch = null;
|
|
326
|
+
let ambiguous = false;
|
|
327
|
+
for (const running of runningProcs) {
|
|
328
|
+
const cmdLower = running.cmdLine.toLowerCase();
|
|
329
|
+
let score = 0;
|
|
330
|
+
for (const part of cmdParts) {
|
|
331
|
+
if (part.length > 2 && cmdLower.includes(part.toLowerCase()))
|
|
332
|
+
score++;
|
|
333
|
+
}
|
|
334
|
+
if (workdirLast && cmdLower.includes(workdirLast))
|
|
335
|
+
score += 3;
|
|
336
|
+
if (cmdLower.includes(proc.workdir.toLowerCase().replace(/\\/g, "/")))
|
|
337
|
+
score += 5;
|
|
338
|
+
if (cmdLower.includes(proc.workdir.toLowerCase()))
|
|
339
|
+
score += 5;
|
|
340
|
+
if (score < 4)
|
|
341
|
+
continue;
|
|
342
|
+
if (!bestMatch || score > bestMatch.score) {
|
|
343
|
+
ambiguous = false;
|
|
344
|
+
bestMatch = { pid: running.pid, score };
|
|
345
|
+
} else if (score === bestMatch.score) {
|
|
346
|
+
ambiguous = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (bestMatch && !ambiguous) {
|
|
350
|
+
result.set(proc.name, bestMatch.pid);
|
|
351
|
+
runningProcs = runningProcs.filter((p) => p.pid !== bestMatch.pid);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
} catch {}
|
|
355
|
+
return result;
|
|
356
|
+
}) ?? new Map;
|
|
357
|
+
}
|
|
358
|
+
async function findPidByPort(port, maxWaitMs = 8000) {
|
|
359
|
+
const start = Date.now();
|
|
360
|
+
const pollMs = 500;
|
|
361
|
+
while (Date.now() - start < maxWaitMs) {
|
|
362
|
+
try {
|
|
363
|
+
if (isWindows()) {
|
|
364
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
365
|
+
for (const line of result.split(`
|
|
366
|
+
`)) {
|
|
367
|
+
if (line.includes(`:${port}`) && line.includes("LISTENING")) {
|
|
368
|
+
const parts = line.trim().split(/\s+/);
|
|
369
|
+
const pid = parseInt(parts[parts.length - 1]);
|
|
370
|
+
if (!isNaN(pid) && pid > 0)
|
|
371
|
+
return pid;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
try {
|
|
376
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
377
|
+
for (const line of result2.split(`
|
|
378
|
+
`)) {
|
|
379
|
+
if (line.includes(`:${port}`)) {
|
|
380
|
+
const pidMatch = line.match(/pid=(\d+)/);
|
|
381
|
+
if (pidMatch)
|
|
382
|
+
return parseInt(pidMatch[1]);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} catch {}
|
|
386
|
+
const result = await $`lsof -iTCP:${port} -sTCP:LISTEN -t`.nothrow().quiet().text();
|
|
387
|
+
const pid = parseInt(result.trim());
|
|
388
|
+
if (!isNaN(pid) && pid > 0)
|
|
389
|
+
return pid;
|
|
390
|
+
}
|
|
391
|
+
} catch {}
|
|
392
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
async function readFileTail(filePath, lines) {
|
|
397
|
+
return await plat.measure(`Read tail ${lines ?? "all"}L`, async () => {
|
|
398
|
+
try {
|
|
399
|
+
const content = await Bun.file(filePath).text();
|
|
400
|
+
if (!lines) {
|
|
401
|
+
return content;
|
|
402
|
+
}
|
|
403
|
+
const allLines = content.split(/\r?\n/);
|
|
404
|
+
const tailLines = allLines.slice(-lines);
|
|
405
|
+
return tailLines.join(`
|
|
406
|
+
`);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
throw new Error(`Error reading file: ${error}`);
|
|
409
|
+
}
|
|
410
|
+
}) ?? "";
|
|
411
|
+
}
|
|
412
|
+
function copyFile(src, dest) {
|
|
413
|
+
fs.copyFileSync(src, dest);
|
|
414
|
+
}
|
|
415
|
+
async function getProcessMemory(pid) {
|
|
416
|
+
const map = await getProcessBatchResources([pid]);
|
|
417
|
+
return map.get(pid)?.memory || 0;
|
|
418
|
+
}
|
|
419
|
+
async function getProcessBatchResources(pids) {
|
|
420
|
+
if (pids.length === 0)
|
|
421
|
+
return new Map;
|
|
422
|
+
return await plat.measure(`Batch resources (${pids.length} PIDs)`, async () => {
|
|
423
|
+
const resourceMap = new Map;
|
|
424
|
+
const pidSet = new Set(pids);
|
|
425
|
+
try {
|
|
426
|
+
if (isWindows()) {
|
|
427
|
+
const output = psExec(`Get-Process -Id ${pids.join(",")} -ErrorAction SilentlyContinue | Select-Object Id, WorkingSet64 | ForEach-Object { Write-Output "$($_.Id)|$($_.WorkingSet64)" }`);
|
|
428
|
+
for (const line of output.split(`
|
|
429
|
+
`)) {
|
|
430
|
+
const sepIdx = line.indexOf("|");
|
|
431
|
+
if (sepIdx === -1)
|
|
432
|
+
continue;
|
|
433
|
+
const pid = parseInt(line.substring(0, sepIdx).trim());
|
|
434
|
+
const memory = parseInt(line.substring(sepIdx + 1).trim()) || 0;
|
|
435
|
+
if (!isNaN(pid) && pidSet.has(pid)) {
|
|
436
|
+
resourceMap.set(pid, { memory, cpu: 0 });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
const result = await $`ps -eo pid,pcpu,rss`.nothrow().quiet().text();
|
|
441
|
+
const lines = result.trim().split(`
|
|
442
|
+
`);
|
|
443
|
+
for (let i = 1;i < lines.length; i++) {
|
|
444
|
+
const line = lines[i].trim();
|
|
445
|
+
if (!line)
|
|
446
|
+
continue;
|
|
447
|
+
const [pidStr, cpuStr, rssStr] = line.split(/\s+/);
|
|
448
|
+
const pid = parseInt(pidStr);
|
|
449
|
+
const cpu = parseFloat(cpuStr) || 0;
|
|
450
|
+
const rss = parseInt(rssStr) || 0;
|
|
451
|
+
if (pidSet.has(pid)) {
|
|
452
|
+
resourceMap.set(pid, { memory: rss * 1024, cpu });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} catch (e) {}
|
|
457
|
+
return resourceMap;
|
|
458
|
+
}) ?? new Map;
|
|
459
|
+
}
|
|
460
|
+
function parseUnixListeningPorts(output) {
|
|
461
|
+
const ports = new Set;
|
|
462
|
+
for (const line of output.split(`
|
|
463
|
+
`)) {
|
|
464
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
465
|
+
if (portMatch) {
|
|
466
|
+
ports.add(parseInt(portMatch[1]));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return Array.from(ports);
|
|
470
|
+
}
|
|
471
|
+
async function getProcessPorts(pid) {
|
|
472
|
+
try {
|
|
473
|
+
if (isWindows()) {
|
|
474
|
+
const result = await $`netstat -ano`.nothrow().quiet().text();
|
|
475
|
+
const ports = new Set;
|
|
476
|
+
for (const line of result.split(`
|
|
477
|
+
`)) {
|
|
478
|
+
const match = line.match(/^\s*TCP\s+\S+:(\d+)\s+\S+\s+LISTENING\s+(\d+)/);
|
|
479
|
+
if (match && parseInt(match[2]) === pid) {
|
|
480
|
+
ports.add(parseInt(match[1]));
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return Array.from(ports);
|
|
484
|
+
} else {
|
|
485
|
+
try {
|
|
486
|
+
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
487
|
+
const ports = new Set;
|
|
488
|
+
for (const line of result2.split(`
|
|
489
|
+
`)) {
|
|
490
|
+
if (line.includes(`pid=${pid}`)) {
|
|
491
|
+
const portMatch = line.match(/:(\d+)\s/);
|
|
492
|
+
if (portMatch) {
|
|
493
|
+
ports.add(parseInt(portMatch[1]));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (ports.size > 0)
|
|
498
|
+
return Array.from(ports);
|
|
499
|
+
} catch {}
|
|
500
|
+
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
501
|
+
return parseUnixListeningPorts(result);
|
|
502
|
+
}
|
|
503
|
+
} catch {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
var plat;
|
|
508
|
+
var init_platform = __esm(() => {
|
|
509
|
+
plat = createMeasure("platform");
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// src/db.ts
|
|
513
|
+
var exports_db = {};
|
|
514
|
+
__export(exports_db, {
|
|
515
|
+
updateProcessPid: () => updateProcessPid,
|
|
516
|
+
updateProcessEnv: () => updateProcessEnv,
|
|
517
|
+
saveTemplate: () => saveTemplate,
|
|
518
|
+
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
519
|
+
removeProcessByName: () => removeProcessByName,
|
|
520
|
+
removeProcess: () => removeProcess,
|
|
521
|
+
removeDependency: () => removeDependency,
|
|
522
|
+
removeAllProcesses: () => removeAllProcesses,
|
|
523
|
+
removeAllDependencies: () => removeAllDependencies,
|
|
524
|
+
insertProcess: () => insertProcess,
|
|
525
|
+
getTemplate: () => getTemplate,
|
|
526
|
+
getStartOrder: () => getStartOrder,
|
|
527
|
+
getRecentHistory: () => getRecentHistory,
|
|
528
|
+
getProcessHistory: () => getProcessHistory,
|
|
529
|
+
getProcess: () => getProcess,
|
|
530
|
+
getDependents: () => getDependents,
|
|
531
|
+
getDependencyGraph: () => getDependencyGraph,
|
|
532
|
+
getDependencies: () => getDependencies,
|
|
533
|
+
getDbInfo: () => getDbInfo,
|
|
534
|
+
getAllTemplates: () => getAllTemplates,
|
|
535
|
+
getAllProcesses: () => getAllProcesses,
|
|
536
|
+
deleteTemplate: () => deleteTemplate,
|
|
537
|
+
dbPath: () => dbPath,
|
|
538
|
+
db: () => db,
|
|
539
|
+
clearOldHistory: () => clearOldHistory,
|
|
540
|
+
bgrHome: () => bgrHome,
|
|
541
|
+
addHistoryEntry: () => addHistoryEntry,
|
|
542
|
+
addDependency: () => addDependency,
|
|
543
|
+
TemplateSchema: () => TemplateSchema,
|
|
544
|
+
ProcessSchema: () => ProcessSchema,
|
|
545
|
+
HistorySchema: () => HistorySchema,
|
|
546
|
+
DependencySchema: () => DependencySchema
|
|
547
|
+
});
|
|
548
|
+
import { Database, z } from "sqlite-zod-orm";
|
|
549
|
+
import { join as join2 } from "path";
|
|
550
|
+
var {sleep } = globalThis.Bun;
|
|
551
|
+
import { existsSync as existsSync2, copyFileSync as copyFileSync2 } from "fs";
|
|
552
|
+
function shouldAutoMigrateLegacyDb() {
|
|
553
|
+
const raw = (process.env.BGRUN_DISABLE_LEGACY_MIGRATION || "").trim().toLowerCase();
|
|
554
|
+
return !(raw === "1" || raw === "true" || raw === "yes");
|
|
555
|
+
}
|
|
556
|
+
function getProcess(name) {
|
|
557
|
+
return db.process.select().where({ name }).orderBy("timestamp", "desc").limit(1).get() || null;
|
|
558
|
+
}
|
|
559
|
+
function getAllProcesses() {
|
|
560
|
+
return db.process.select().all();
|
|
561
|
+
}
|
|
562
|
+
function insertProcess(data) {
|
|
563
|
+
return db.process.insert({
|
|
564
|
+
...data,
|
|
565
|
+
timestamp: new Date().toISOString()
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function removeProcess(pid) {
|
|
569
|
+
const matches = db.process.select().where({ pid }).all();
|
|
570
|
+
for (const p of matches) {
|
|
571
|
+
db.process.delete(p.id);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function removeProcessByName(name) {
|
|
575
|
+
const matches = db.process.select().where({ name }).all();
|
|
576
|
+
for (const p of matches) {
|
|
577
|
+
db.process.delete(p.id);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function updateProcessPid(name, newPid) {
|
|
581
|
+
const proc = db.process.select().where({ name }).limit(1).get();
|
|
582
|
+
if (proc) {
|
|
583
|
+
db.process.update(proc.id, { pid: newPid });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function removeAllProcesses() {
|
|
587
|
+
const all = db.process.select().all();
|
|
588
|
+
for (const p of all) {
|
|
589
|
+
db.process.delete(p.id);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function updateProcessEnv(name, envJson) {
|
|
593
|
+
const proc = db.process.select().where({ name }).limit(1).get();
|
|
594
|
+
if (proc) {
|
|
595
|
+
db.process.update(proc.id, { env: envJson });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function getAllTemplates() {
|
|
599
|
+
return db.template.select().all();
|
|
600
|
+
}
|
|
601
|
+
function getTemplate(name) {
|
|
602
|
+
return db.template.select().where({ name }).limit(1).get() || null;
|
|
603
|
+
}
|
|
604
|
+
function saveTemplate(data) {
|
|
605
|
+
const existing = db.template.select().where({ name: data.name }).limit(1).get();
|
|
606
|
+
if (existing) {
|
|
607
|
+
db.template.update(existing.id, {
|
|
608
|
+
command: data.command,
|
|
609
|
+
workdir: data.workdir || "",
|
|
610
|
+
env: data.env || "",
|
|
611
|
+
group: data.group || ""
|
|
612
|
+
});
|
|
613
|
+
} else {
|
|
614
|
+
db.template.insert({
|
|
615
|
+
name: data.name,
|
|
616
|
+
command: data.command,
|
|
617
|
+
workdir: data.workdir || "",
|
|
618
|
+
env: data.env || "",
|
|
619
|
+
group: data.group || ""
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function deleteTemplate(name) {
|
|
624
|
+
const tmpl = db.template.select().where({ name }).limit(1).get();
|
|
625
|
+
if (tmpl) {
|
|
626
|
+
db.template.delete(tmpl.id);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function getProcessHistory(name, limit = 50) {
|
|
630
|
+
return db.history.select().where({ process_name: name }).orderBy("timestamp", "desc").limit(limit).all();
|
|
631
|
+
}
|
|
632
|
+
function addHistoryEntry(processName, event, pid, metadata = {}) {
|
|
633
|
+
return db.history.insert({
|
|
634
|
+
process_name: processName,
|
|
635
|
+
event,
|
|
636
|
+
pid,
|
|
637
|
+
metadata: JSON.stringify(metadata)
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
function getRecentHistory(limit = 100) {
|
|
641
|
+
return db.history.select().orderBy("timestamp", "desc").limit(limit).all();
|
|
642
|
+
}
|
|
643
|
+
function clearOldHistory(daysToKeep = 30) {
|
|
644
|
+
const cutoff = new Date;
|
|
645
|
+
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
646
|
+
const cutoffStr = cutoff.toISOString();
|
|
647
|
+
const oldEntries = db.history.select().where("timestamp", "<", cutoffStr).all();
|
|
648
|
+
for (const entry of oldEntries) {
|
|
649
|
+
db.history.delete(entry.id);
|
|
650
|
+
}
|
|
651
|
+
return oldEntries.length;
|
|
652
|
+
}
|
|
653
|
+
function getDependencies(processName) {
|
|
654
|
+
return db.dependency.select().where({ process_name: processName }).all().map((d) => d.depends_on);
|
|
655
|
+
}
|
|
656
|
+
function getDependents(processName) {
|
|
657
|
+
return db.dependency.select().where({ depends_on: processName }).all().map((d) => d.process_name);
|
|
658
|
+
}
|
|
659
|
+
function getDependencyGraph() {
|
|
660
|
+
const all = db.dependency.select().all();
|
|
661
|
+
const graph = {};
|
|
662
|
+
for (const dep of all) {
|
|
663
|
+
if (!graph[dep.process_name])
|
|
664
|
+
graph[dep.process_name] = [];
|
|
665
|
+
graph[dep.process_name].push(dep.depends_on);
|
|
666
|
+
}
|
|
667
|
+
return graph;
|
|
668
|
+
}
|
|
669
|
+
function addDependency(processName, dependsOn) {
|
|
670
|
+
if (processName === dependsOn)
|
|
671
|
+
return false;
|
|
672
|
+
const existing = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).limit(1).get();
|
|
673
|
+
if (existing)
|
|
674
|
+
return false;
|
|
675
|
+
if (wouldCreateCycle(processName, dependsOn))
|
|
676
|
+
return false;
|
|
677
|
+
db.dependency.insert({ process_name: processName, depends_on: dependsOn });
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
function removeDependency(processName, dependsOn) {
|
|
681
|
+
const matches = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).all();
|
|
682
|
+
for (const dep of matches) {
|
|
683
|
+
db.dependency.delete(dep.id);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function removeAllDependencies(processName) {
|
|
687
|
+
const matches = db.dependency.select().where({ process_name: processName }).all();
|
|
688
|
+
for (const dep of matches) {
|
|
689
|
+
db.dependency.delete(dep.id);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
function wouldCreateCycle(processName, dependsOn) {
|
|
693
|
+
const graph = getDependencyGraph();
|
|
694
|
+
if (!graph[processName])
|
|
695
|
+
graph[processName] = [];
|
|
696
|
+
graph[processName].push(dependsOn);
|
|
697
|
+
const visited = new Set;
|
|
698
|
+
const stack = [dependsOn];
|
|
699
|
+
while (stack.length > 0) {
|
|
700
|
+
const current = stack.pop();
|
|
701
|
+
if (current === processName)
|
|
702
|
+
return true;
|
|
703
|
+
if (visited.has(current))
|
|
704
|
+
continue;
|
|
705
|
+
visited.add(current);
|
|
706
|
+
for (const dep of graph[current] || []) {
|
|
707
|
+
stack.push(dep);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
function getStartOrder() {
|
|
713
|
+
const graph = getDependencyGraph();
|
|
714
|
+
const allProcesses = getAllProcesses().map((p) => p.name);
|
|
715
|
+
const allNames = new Set(allProcesses);
|
|
716
|
+
const inDegree = {};
|
|
717
|
+
for (const name of allNames)
|
|
718
|
+
inDegree[name] = 0;
|
|
719
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
720
|
+
for (const dep of deps) {
|
|
721
|
+
if (allNames.has(dep)) {
|
|
722
|
+
inDegree[proc] = (inDegree[proc] || 0) + 1;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const queue = [];
|
|
727
|
+
for (const name of allNames) {
|
|
728
|
+
if ((inDegree[name] || 0) === 0)
|
|
729
|
+
queue.push(name);
|
|
730
|
+
}
|
|
731
|
+
const order = [];
|
|
732
|
+
while (queue.length > 0) {
|
|
733
|
+
queue.sort();
|
|
734
|
+
const current = queue.shift();
|
|
735
|
+
order.push(current);
|
|
736
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
737
|
+
if (deps.includes(current) && allNames.has(proc)) {
|
|
738
|
+
inDegree[proc]--;
|
|
739
|
+
if (inDegree[proc] === 0)
|
|
740
|
+
queue.push(proc);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return order;
|
|
745
|
+
}
|
|
746
|
+
function getDbInfo() {
|
|
747
|
+
return {
|
|
748
|
+
dbPath,
|
|
749
|
+
bgrHome,
|
|
750
|
+
dbFilename,
|
|
751
|
+
exists: existsSync2(dbPath)
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
755
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
756
|
+
try {
|
|
757
|
+
return operation();
|
|
758
|
+
} catch (err) {
|
|
759
|
+
if (err?.code === "SQLITE_BUSY" && attempt < maxRetries) {
|
|
760
|
+
await sleep(delay * attempt);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
throw err;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
throw new Error("Max retries reached for database operation");
|
|
767
|
+
}
|
|
768
|
+
var ProcessSchema, TemplateSchema, HistorySchema, DependencySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
769
|
+
var init_db = __esm(() => {
|
|
770
|
+
init_platform();
|
|
771
|
+
ProcessSchema = z.object({
|
|
772
|
+
pid: z.number(),
|
|
773
|
+
workdir: z.string(),
|
|
774
|
+
command: z.string(),
|
|
775
|
+
name: z.string(),
|
|
776
|
+
env: z.string(),
|
|
777
|
+
configPath: z.string().default(""),
|
|
778
|
+
stdout_path: z.string(),
|
|
779
|
+
stderr_path: z.string(),
|
|
780
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
781
|
+
group: z.string().default("")
|
|
782
|
+
});
|
|
783
|
+
TemplateSchema = z.object({
|
|
784
|
+
name: z.string(),
|
|
785
|
+
command: z.string(),
|
|
786
|
+
workdir: z.string().default(""),
|
|
787
|
+
env: z.string().default(""),
|
|
788
|
+
group: z.string().default(""),
|
|
789
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
790
|
+
});
|
|
791
|
+
HistorySchema = z.object({
|
|
792
|
+
process_name: z.string(),
|
|
793
|
+
event: z.string(),
|
|
794
|
+
pid: z.number().optional(),
|
|
795
|
+
timestamp: z.string().default(() => new Date().toISOString()),
|
|
796
|
+
metadata: z.string().default("")
|
|
797
|
+
});
|
|
798
|
+
DependencySchema = z.object({
|
|
799
|
+
process_name: z.string(),
|
|
800
|
+
depends_on: z.string(),
|
|
801
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
802
|
+
});
|
|
803
|
+
homePath = getHomeDir();
|
|
804
|
+
bgrDir = join2(homePath, ".bgr");
|
|
805
|
+
ensureDir(bgrDir);
|
|
806
|
+
dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
|
|
807
|
+
dbPath = join2(bgrDir, dbFilename);
|
|
808
|
+
bgrHome = bgrDir;
|
|
809
|
+
legacyDbPath = join2(bgrDir, "bgr_v2.sqlite");
|
|
810
|
+
if (shouldAutoMigrateLegacyDb() && !existsSync2(dbPath) && existsSync2(legacyDbPath)) {
|
|
811
|
+
try {
|
|
812
|
+
copyFileSync2(legacyDbPath, dbPath);
|
|
813
|
+
console.log(`[bgrun] Migrated database: ${legacyDbPath} \u2192 ${dbPath}`);
|
|
814
|
+
} catch (e) {}
|
|
815
|
+
}
|
|
816
|
+
db = new Database(dbPath, {
|
|
817
|
+
process: ProcessSchema,
|
|
818
|
+
template: TemplateSchema,
|
|
819
|
+
history: HistorySchema,
|
|
820
|
+
dependency: DependencySchema
|
|
821
|
+
}, {
|
|
822
|
+
indexes: {
|
|
823
|
+
process: ["name", "timestamp", "pid"],
|
|
824
|
+
template: ["name"],
|
|
825
|
+
history: ["process_name", "timestamp"],
|
|
826
|
+
dependency: ["process_name", "depends_on"]
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// src/utils.ts
|
|
832
|
+
import * as fs2 from "fs";
|
|
833
|
+
function parseEnvString(envString) {
|
|
834
|
+
const env = {};
|
|
835
|
+
envString.split(",").forEach((pair) => {
|
|
836
|
+
const [key, value] = pair.split("=");
|
|
837
|
+
if (key && value)
|
|
838
|
+
env[key] = value;
|
|
839
|
+
});
|
|
840
|
+
return env;
|
|
841
|
+
}
|
|
842
|
+
function calculateRuntime(startTime) {
|
|
843
|
+
const start = new Date(startTime).getTime();
|
|
844
|
+
const now = new Date().getTime();
|
|
845
|
+
const diffInMinutes = Math.floor((now - start) / (1000 * 60));
|
|
846
|
+
return `${diffInMinutes} minutes`;
|
|
847
|
+
}
|
|
848
|
+
async function getVersion() {
|
|
849
|
+
try {
|
|
850
|
+
const { join: join3 } = await import("path");
|
|
851
|
+
const pkgPath = join3(import.meta.dir, "../package.json");
|
|
852
|
+
const pkg = await Bun.file(pkgPath).json();
|
|
853
|
+
return pkg.version || "0.0.0";
|
|
854
|
+
} catch {
|
|
855
|
+
return "0.0.0";
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function validateDirectory(directory) {
|
|
859
|
+
if (!directory || !fs2.existsSync(directory)) {
|
|
860
|
+
throw new Error(`Directory not found or invalid: '${directory}'`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
function tailFile(path, prefix, colorFn, lines) {
|
|
864
|
+
let position = 0;
|
|
865
|
+
let lastPartial = "";
|
|
866
|
+
if (!fs2.existsSync(path)) {
|
|
867
|
+
return () => {};
|
|
868
|
+
}
|
|
869
|
+
const fd = fs2.openSync(path, "r");
|
|
870
|
+
const printNewContent = () => {
|
|
871
|
+
try {
|
|
872
|
+
const stats = fs2.statSync(path);
|
|
873
|
+
if (stats.size <= position)
|
|
874
|
+
return;
|
|
875
|
+
const buffer = Buffer.alloc(stats.size - position);
|
|
876
|
+
fs2.readSync(fd, buffer, 0, buffer.length, position);
|
|
877
|
+
let content = buffer.toString();
|
|
878
|
+
content = lastPartial + content;
|
|
879
|
+
lastPartial = "";
|
|
880
|
+
const lineArray = content.split(/\r?\n/);
|
|
881
|
+
if (!content.endsWith(`
|
|
882
|
+
`)) {
|
|
883
|
+
lastPartial = lineArray.pop() || "";
|
|
884
|
+
}
|
|
885
|
+
lineArray.forEach((line) => {
|
|
886
|
+
if (line) {
|
|
887
|
+
console.log(colorFn(prefix + line));
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
position = stats.size;
|
|
891
|
+
} catch (e) {}
|
|
892
|
+
};
|
|
893
|
+
const watcher = fs2.watch(path, { persistent: true }, (event) => {
|
|
894
|
+
if (event === "change") {
|
|
895
|
+
printNewContent();
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
printNewContent();
|
|
899
|
+
return () => {
|
|
900
|
+
watcher.close();
|
|
901
|
+
try {
|
|
902
|
+
fs2.closeSync(fd);
|
|
903
|
+
} catch {}
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
var init_utils = __esm(() => {
|
|
907
|
+
init_platform();
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
// src/deps.ts
|
|
911
|
+
var exports_deps = {};
|
|
912
|
+
__export(exports_deps, {
|
|
913
|
+
getUnmetDeps: () => getUnmetDeps,
|
|
914
|
+
getDependencies: () => getDependencies2,
|
|
915
|
+
buildDepGraph: () => buildDepGraph
|
|
916
|
+
});
|
|
917
|
+
function getDependencies2(envStr) {
|
|
918
|
+
const env = parseEnvString(envStr);
|
|
919
|
+
const raw = env.BGR_DEPENDS_ON || "";
|
|
920
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
921
|
+
}
|
|
922
|
+
async function buildDepGraph() {
|
|
923
|
+
const processes = getAllProcesses();
|
|
924
|
+
const nodeMap = new Map;
|
|
925
|
+
for (const proc of processes) {
|
|
926
|
+
const deps = getDependencies2(proc.env);
|
|
927
|
+
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
928
|
+
nodeMap.set(proc.name, {
|
|
929
|
+
name: proc.name,
|
|
930
|
+
dependsOn: deps,
|
|
931
|
+
dependedBy: [],
|
|
932
|
+
running: alive,
|
|
933
|
+
pid: proc.pid
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
for (const node of nodeMap.values()) {
|
|
937
|
+
for (const dep of node.dependsOn) {
|
|
938
|
+
const depNode = nodeMap.get(dep);
|
|
939
|
+
if (depNode) {
|
|
940
|
+
depNode.dependedBy.push(node.name);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const inDegree = new Map;
|
|
945
|
+
for (const node of nodeMap.values()) {
|
|
946
|
+
inDegree.set(node.name, node.dependsOn.filter((d) => nodeMap.has(d)).length);
|
|
947
|
+
}
|
|
948
|
+
const queue = [];
|
|
949
|
+
for (const [name, degree] of inDegree) {
|
|
950
|
+
if (degree === 0)
|
|
951
|
+
queue.push(name);
|
|
952
|
+
}
|
|
953
|
+
const order = [];
|
|
954
|
+
while (queue.length > 0) {
|
|
955
|
+
const current = queue.shift();
|
|
956
|
+
order.push(current);
|
|
957
|
+
const node = nodeMap.get(current);
|
|
958
|
+
for (const dependent of node.dependedBy) {
|
|
959
|
+
const newDegree = (inDegree.get(dependent) || 0) - 1;
|
|
960
|
+
inDegree.set(dependent, newDegree);
|
|
961
|
+
if (newDegree === 0)
|
|
962
|
+
queue.push(dependent);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const hasCycle = order.length < nodeMap.size;
|
|
966
|
+
const cycleNodes = hasCycle ? [...nodeMap.keys()].filter((n) => !order.includes(n)) : undefined;
|
|
967
|
+
return {
|
|
968
|
+
nodes: [...nodeMap.values()],
|
|
969
|
+
order,
|
|
970
|
+
hasCycle,
|
|
971
|
+
cycleNodes
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
async function getUnmetDeps(name) {
|
|
975
|
+
const proc = getProcess(name);
|
|
976
|
+
if (!proc)
|
|
977
|
+
return [];
|
|
978
|
+
const deps = getDependencies2(proc.env);
|
|
979
|
+
const unmet = [];
|
|
980
|
+
for (const depName of deps) {
|
|
981
|
+
const depProc = getProcess(depName);
|
|
982
|
+
if (!depProc) {
|
|
983
|
+
unmet.push(depName);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
const alive = await isProcessRunning(depProc.pid, depProc.command);
|
|
987
|
+
if (!alive) {
|
|
988
|
+
unmet.push(depName);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return unmet;
|
|
992
|
+
}
|
|
993
|
+
var init_deps = __esm(() => {
|
|
994
|
+
init_db();
|
|
995
|
+
init_platform();
|
|
996
|
+
init_utils();
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// src/logger.ts
|
|
1000
|
+
import boxen from "boxen";
|
|
1001
|
+
import chalk from "chalk";
|
|
1002
|
+
function announce(message, title) {
|
|
1003
|
+
console.log(boxen(message, {
|
|
1004
|
+
padding: 1,
|
|
1005
|
+
margin: 1,
|
|
1006
|
+
borderColor: "green",
|
|
1007
|
+
title: title || "bgrun",
|
|
1008
|
+
titleAlignment: "center",
|
|
1009
|
+
borderStyle: "round"
|
|
1010
|
+
}));
|
|
1011
|
+
}
|
|
1012
|
+
function error(message) {
|
|
1013
|
+
const text = message instanceof Error ? message.stack || message.message : String(message);
|
|
1014
|
+
console.error(boxen(chalk.red(text), {
|
|
1015
|
+
padding: 1,
|
|
1016
|
+
margin: 1,
|
|
1017
|
+
borderColor: "red",
|
|
1018
|
+
title: "Error",
|
|
1019
|
+
titleAlignment: "center",
|
|
1020
|
+
borderStyle: "double"
|
|
1021
|
+
}));
|
|
1022
|
+
throw new BgrunError(text);
|
|
1023
|
+
}
|
|
1024
|
+
var BgrunError;
|
|
1025
|
+
var init_logger = __esm(() => {
|
|
1026
|
+
BgrunError = class BgrunError extends Error {
|
|
1027
|
+
constructor(message) {
|
|
1028
|
+
super(message);
|
|
1029
|
+
this.name = "BgrunError";
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
// src/config.ts
|
|
1035
|
+
function formatEnvKey(key) {
|
|
1036
|
+
return key.toUpperCase().replace(/\./g, "_");
|
|
1037
|
+
}
|
|
1038
|
+
function flattenConfig(obj, prefix = "") {
|
|
1039
|
+
return Object.keys(obj).reduce((acc, key) => {
|
|
1040
|
+
const value = obj[key];
|
|
1041
|
+
const newPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1042
|
+
if (Array.isArray(value)) {
|
|
1043
|
+
value.forEach((item, index) => {
|
|
1044
|
+
const indexedPrefix = `${newPrefix}.${index}`;
|
|
1045
|
+
if (typeof item === "object" && item !== null) {
|
|
1046
|
+
Object.assign(acc, flattenConfig(item, indexedPrefix));
|
|
1047
|
+
} else {
|
|
1048
|
+
acc[formatEnvKey(indexedPrefix)] = String(item);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1052
|
+
Object.assign(acc, flattenConfig(value, newPrefix));
|
|
1053
|
+
} else {
|
|
1054
|
+
acc[formatEnvKey(newPrefix)] = String(value);
|
|
1055
|
+
}
|
|
1056
|
+
return acc;
|
|
1057
|
+
}, {});
|
|
1058
|
+
}
|
|
1059
|
+
async function parseConfigFile(configPath) {
|
|
1060
|
+
const importPath = `${configPath}?t=${Date.now()}`;
|
|
1061
|
+
const parsedConfig = await import(importPath).then((m) => m.default);
|
|
1062
|
+
return flattenConfig(parsedConfig);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/commands/run.ts
|
|
1066
|
+
var {$: $2 } = globalThis.Bun;
|
|
1067
|
+
var {sleep: sleep2 } = globalThis.Bun;
|
|
1068
|
+
import { join as join3 } from "path";
|
|
1069
|
+
import { createMeasure as createMeasure2 } from "measure-fn";
|
|
1070
|
+
async function handleRun(options) {
|
|
1071
|
+
const { command, directory, env, name, configPath, force, fetch, stdout, stderr } = options;
|
|
1072
|
+
const existingProcess = name ? getProcess(name) : null;
|
|
1073
|
+
if (name && existingProcess) {
|
|
1074
|
+
const { getUnmetDeps: getUnmetDeps2 } = await Promise.resolve().then(() => (init_deps(), exports_deps));
|
|
1075
|
+
const unmet = await getUnmetDeps2(name);
|
|
1076
|
+
if (unmet.length > 0) {
|
|
1077
|
+
await run.measure(`Start ${unmet.length} dependencies for "${name}"`, async () => {
|
|
1078
|
+
for (const depName of unmet) {
|
|
1079
|
+
const depProc = getProcess(depName);
|
|
1080
|
+
if (depProc) {
|
|
1081
|
+
announce(`\uD83D\uDCE6 Starting dependency "${depName}" for "${name}"`, "Dependency");
|
|
1082
|
+
await handleRun({ action: "run", name: depName, force: true, remoteName: "" });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (existingProcess) {
|
|
1089
|
+
const finalDirectory2 = directory || existingProcess.workdir;
|
|
1090
|
+
validateDirectory(finalDirectory2);
|
|
1091
|
+
$2.cwd(finalDirectory2);
|
|
1092
|
+
if (fetch) {
|
|
1093
|
+
if (!__require("fs").existsSync(__require("path").join(finalDirectory2, ".git"))) {
|
|
1094
|
+
error(`Cannot --fetch: '${finalDirectory2}' is not a Git repository.`);
|
|
1095
|
+
}
|
|
1096
|
+
await run.measure(`Git fetch "${name}"`, async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
await $2`git fetch origin`;
|
|
1099
|
+
const localHash = (await $2`git rev-parse HEAD`.text()).trim();
|
|
1100
|
+
const remoteHash = (await $2`git rev-parse origin/$(git rev-parse --abbrev-ref HEAD)`.text()).trim();
|
|
1101
|
+
if (localHash !== remoteHash) {
|
|
1102
|
+
await $2`git pull origin $(git rev-parse --abbrev-ref HEAD)`;
|
|
1103
|
+
announce("\uD83D\uDCE5 Pulled latest changes", "Git Update");
|
|
1104
|
+
}
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
error(`Failed to pull latest changes: ${err}`);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const isRunning = await isProcessRunning(existingProcess.pid);
|
|
1111
|
+
if (isRunning && !force) {
|
|
1112
|
+
error(`Process '${name}' is currently running. Use --force to restart.`);
|
|
1113
|
+
}
|
|
1114
|
+
let detectedPorts = [];
|
|
1115
|
+
if (isRunning) {
|
|
1116
|
+
detectedPorts = await getProcessPorts(existingProcess.pid);
|
|
1117
|
+
}
|
|
1118
|
+
if (isRunning) {
|
|
1119
|
+
await run.measure(`Terminate "${name}" (PID ${existingProcess.pid})`, async () => {
|
|
1120
|
+
await terminateProcess(existingProcess.pid);
|
|
1121
|
+
announce(`\uD83D\uDD25 Terminated existing process '${name}'`, "Process Terminated");
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
if (detectedPorts.length > 0) {
|
|
1125
|
+
await run.measure(`Port cleanup [${detectedPorts.join(", ")}]`, async () => {
|
|
1126
|
+
for (const port of detectedPorts) {
|
|
1127
|
+
await killProcessOnPort(port);
|
|
1128
|
+
}
|
|
1129
|
+
for (const port of detectedPorts) {
|
|
1130
|
+
const freed = await waitForPortFree(port, 5000);
|
|
1131
|
+
if (!freed) {
|
|
1132
|
+
await killProcessOnPort(port);
|
|
1133
|
+
await waitForPortFree(port, 3000);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
const cmdToMatch = existingProcess.command;
|
|
1139
|
+
if (cmdToMatch) {
|
|
1140
|
+
await run.measure("Zombie sweep", async () => {
|
|
1141
|
+
try {
|
|
1142
|
+
const cmdKeyword = cmdToMatch.split(" ")[1] || cmdToMatch;
|
|
1143
|
+
const GENERIC_KEYWORDS = ["dev", "run", "start", "serve", "build", "test"];
|
|
1144
|
+
if (GENERIC_KEYWORDS.includes(cmdKeyword.toLowerCase())) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const currentPid = process.pid;
|
|
1148
|
+
const result = await psExec(`Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { $_.CommandLine -match '${cmdKeyword.replace(/'/g, "''")}' -and $_.ProcessId -ne ${currentPid} } | Select-Object -ExpandProperty ProcessId`, 3000);
|
|
1149
|
+
const zombiePids = result.split(`
|
|
1150
|
+
`).map((l) => parseInt(l.trim())).filter((n) => !isNaN(n) && n > 0 && n !== currentPid);
|
|
1151
|
+
for (const zPid of zombiePids) {
|
|
1152
|
+
await $2`taskkill /F /T /PID ${zPid}`.nothrow().quiet();
|
|
1153
|
+
}
|
|
1154
|
+
if (zombiePids.length > 0) {
|
|
1155
|
+
announce(`\uD83E\uDDF9 Swept ${zombiePids.length} zombie process(es)`, "Zombie Cleanup");
|
|
1156
|
+
}
|
|
1157
|
+
} catch {}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
await retryDatabaseOperation(() => removeProcessByName(name));
|
|
1161
|
+
} else {
|
|
1162
|
+
if (!directory || !name || !command) {
|
|
1163
|
+
error("'directory', 'name', and 'command' parameters are required for new processes.");
|
|
1164
|
+
}
|
|
1165
|
+
validateDirectory(directory);
|
|
1166
|
+
$2.cwd(directory);
|
|
1167
|
+
}
|
|
1168
|
+
const finalCommand = command || existingProcess.command;
|
|
1169
|
+
const finalDirectory = directory || existingProcess?.workdir;
|
|
1170
|
+
let finalEnv = env || (existingProcess ? parseEnvString(existingProcess.env) : {});
|
|
1171
|
+
if (!("BGR_KEEP_ALIVE" in finalEnv)) {
|
|
1172
|
+
finalEnv.BGR_KEEP_ALIVE = "true";
|
|
1173
|
+
}
|
|
1174
|
+
let finalConfigPath;
|
|
1175
|
+
if (configPath !== undefined) {
|
|
1176
|
+
finalConfigPath = configPath;
|
|
1177
|
+
} else if (existingProcess) {
|
|
1178
|
+
finalConfigPath = existingProcess.configPath;
|
|
1179
|
+
} else {
|
|
1180
|
+
finalConfigPath = ".config.toml";
|
|
1181
|
+
}
|
|
1182
|
+
if (finalConfigPath) {
|
|
1183
|
+
const fullConfigPath = join3(finalDirectory, finalConfigPath);
|
|
1184
|
+
if (await Bun.file(fullConfigPath).exists()) {
|
|
1185
|
+
const configEnv = await run.measure(`Parse config "${finalConfigPath}"`, async () => {
|
|
1186
|
+
try {
|
|
1187
|
+
return await parseConfigFile(fullConfigPath);
|
|
1188
|
+
} catch (err) {
|
|
1189
|
+
console.warn(`Warning: Failed to parse config file ${finalConfigPath}: ${err.message}`);
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
if (configEnv) {
|
|
1194
|
+
finalEnv = { ...finalEnv, ...configEnv };
|
|
1195
|
+
console.log(`Loaded config from ${finalConfigPath}`);
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
console.log(`Config file '${finalConfigPath}' not found, continuing without it.`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const stdoutPath = stdout || existingProcess?.stdout_path || join3(homePath2, ".bgr", `${name}-out.txt`);
|
|
1202
|
+
Bun.write(stdoutPath, "");
|
|
1203
|
+
const stderrPath = stderr || existingProcess?.stderr_path || join3(homePath2, ".bgr", `${name}-err.txt`);
|
|
1204
|
+
Bun.write(stderrPath, "");
|
|
1205
|
+
const actualPid = await run.measure(`Spawn "${name}" \u2192 ${finalCommand}`, async () => {
|
|
1206
|
+
const newProcess = Bun.spawn(getShellCommand(finalCommand), {
|
|
1207
|
+
env: { ...Bun.env, ...finalEnv },
|
|
1208
|
+
cwd: finalDirectory,
|
|
1209
|
+
stdout: Bun.file(stdoutPath),
|
|
1210
|
+
stderr: Bun.file(stderrPath)
|
|
1211
|
+
});
|
|
1212
|
+
newProcess.unref();
|
|
1213
|
+
await sleep2(100);
|
|
1214
|
+
const pid = await findChildPid(newProcess.pid);
|
|
1215
|
+
await sleep2(400);
|
|
1216
|
+
return pid;
|
|
1217
|
+
}) ?? 0;
|
|
1218
|
+
await retryDatabaseOperation(() => insertProcess({
|
|
1219
|
+
pid: actualPid,
|
|
1220
|
+
workdir: finalDirectory,
|
|
1221
|
+
command: finalCommand,
|
|
1222
|
+
name,
|
|
1223
|
+
env: Object.entries(finalEnv).map(([k, v]) => `${k}=${v}`).join(","),
|
|
1224
|
+
configPath: finalConfigPath || "",
|
|
1225
|
+
stdout_path: stdoutPath,
|
|
1226
|
+
stderr_path: stderrPath
|
|
1227
|
+
}));
|
|
1228
|
+
announce(`${existingProcess ? "\uD83D\uDD04 Restarted" : "\uD83D\uDE80 Launched"} process "${name}" with PID ${actualPid}`, "Process Started");
|
|
1229
|
+
}
|
|
1230
|
+
var homePath2, run;
|
|
1231
|
+
var init_run = __esm(() => {
|
|
1232
|
+
init_db();
|
|
1233
|
+
init_platform();
|
|
1234
|
+
init_logger();
|
|
1235
|
+
init_utils();
|
|
1236
|
+
homePath2 = getHomeDir();
|
|
1237
|
+
run = createMeasure2("run");
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
// src/deploy.ts
|
|
1241
|
+
init_db();
|
|
1242
|
+
init_run();
|
|
1243
|
+
var {$: $3 } = globalThis.Bun;
|
|
1244
|
+
function formatDeployToolError(manager, error2) {
|
|
1245
|
+
const raw = error2 instanceof Error ? error2.message : String(error2 ?? "Unknown error");
|
|
1246
|
+
const lower = raw.toLowerCase();
|
|
1247
|
+
if (lower.includes("command not found") || lower.includes("not recognized as an internal or external command") || lower.includes("executable not found") || lower.includes("no such file or directory")) {
|
|
1248
|
+
return `Deploy requires '${manager}', but it is not installed or not available on PATH.`;
|
|
1249
|
+
}
|
|
1250
|
+
return `Dependency install failed with ${manager}: ${raw}`;
|
|
1251
|
+
}
|
|
1252
|
+
function isInternalProcess(name) {
|
|
1253
|
+
return name === "bgr-dashboard" || name === "bgr-guard";
|
|
1254
|
+
}
|
|
1255
|
+
async function pathExists(path) {
|
|
1256
|
+
return await Bun.file(path).exists();
|
|
1257
|
+
}
|
|
1258
|
+
async function isGitRepo(dir) {
|
|
1259
|
+
return await pathExists(`${dir}/.git`) || await pathExists(`${dir}/.git/HEAD`);
|
|
1260
|
+
}
|
|
1261
|
+
async function detectPackageManager(dir) {
|
|
1262
|
+
const hasPackageJson = await pathExists(`${dir}/package.json`);
|
|
1263
|
+
if (!hasPackageJson)
|
|
1264
|
+
return null;
|
|
1265
|
+
if (await pathExists(`${dir}/bun.lock`) || await pathExists(`${dir}/bun.lockb`))
|
|
1266
|
+
return "bun";
|
|
1267
|
+
if (await pathExists(`${dir}/pnpm-lock.yaml`))
|
|
1268
|
+
return "pnpm";
|
|
1269
|
+
if (await pathExists(`${dir}/yarn.lock`))
|
|
1270
|
+
return "yarn";
|
|
1271
|
+
if (await pathExists(`${dir}/package-lock.json`) || await pathExists(`${dir}/npm-shrinkwrap.json`))
|
|
1272
|
+
return "npm";
|
|
1273
|
+
return "bun";
|
|
1274
|
+
}
|
|
1275
|
+
function getInstallCommand(manager) {
|
|
1276
|
+
switch (manager) {
|
|
1277
|
+
case "bun":
|
|
1278
|
+
return "bun install";
|
|
1279
|
+
case "pnpm":
|
|
1280
|
+
return "pnpm install --frozen-lockfile";
|
|
1281
|
+
case "yarn":
|
|
1282
|
+
return "yarn install --frozen-lockfile";
|
|
1283
|
+
case "npm":
|
|
1284
|
+
return "npm ci";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
async function installDependencies(dir) {
|
|
1288
|
+
const manager = await detectPackageManager(dir);
|
|
1289
|
+
if (!manager)
|
|
1290
|
+
return { manager: null, output: "", command: "" };
|
|
1291
|
+
$3.cwd(dir);
|
|
1292
|
+
const command = getInstallCommand(manager);
|
|
1293
|
+
try {
|
|
1294
|
+
switch (manager) {
|
|
1295
|
+
case "bun":
|
|
1296
|
+
return { manager, command, output: (await $3`bun install`.text()).trim() };
|
|
1297
|
+
case "pnpm":
|
|
1298
|
+
return { manager, command, output: (await $3`pnpm install --frozen-lockfile`.text()).trim() };
|
|
1299
|
+
case "yarn":
|
|
1300
|
+
return { manager, command, output: (await $3`yarn install --frozen-lockfile`.text()).trim() };
|
|
1301
|
+
case "npm":
|
|
1302
|
+
return { manager, command, output: (await $3`npm ci`.text()).trim() };
|
|
1303
|
+
default:
|
|
1304
|
+
return { manager: null, output: "", command: "" };
|
|
1305
|
+
}
|
|
1306
|
+
} catch (error2) {
|
|
1307
|
+
throw new Error(formatDeployToolError(manager, error2));
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function deployProcess(name) {
|
|
1311
|
+
const proc = getProcess(name);
|
|
1312
|
+
if (!proc) {
|
|
1313
|
+
return { name, ok: false, reason: `Process '${name}' not found` };
|
|
1314
|
+
}
|
|
1315
|
+
if (isInternalProcess(proc.name)) {
|
|
1316
|
+
return { name, ok: false, skipped: true, reason: "Internal bgrun process skipped" };
|
|
1317
|
+
}
|
|
1318
|
+
const dir = proc.workdir;
|
|
1319
|
+
if (!await isGitRepo(dir)) {
|
|
1320
|
+
return { name, ok: false, skipped: true, reason: `'${dir}' is not a git repository` };
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
$3.cwd(dir);
|
|
1324
|
+
const pullOutput = (await $3`git pull`.text()).trim();
|
|
1325
|
+
const install = await installDependencies(dir);
|
|
1326
|
+
const installOutput = install.output;
|
|
1327
|
+
await handleRun({
|
|
1328
|
+
action: "run",
|
|
1329
|
+
name,
|
|
1330
|
+
force: true,
|
|
1331
|
+
remoteName: ""
|
|
1332
|
+
});
|
|
1333
|
+
addHistoryEntry(name, "deploy", proc.pid, {
|
|
1334
|
+
directory: dir,
|
|
1335
|
+
installed: Boolean(install.manager),
|
|
1336
|
+
packageManager: install.manager,
|
|
1337
|
+
installCommand: install.command
|
|
1338
|
+
});
|
|
1339
|
+
return {
|
|
1340
|
+
name,
|
|
1341
|
+
ok: true,
|
|
1342
|
+
pullOutput,
|
|
1343
|
+
installOutput,
|
|
1344
|
+
packageManager: install.manager,
|
|
1345
|
+
installCommand: install.command,
|
|
1346
|
+
installAttempted: Boolean(install.manager)
|
|
1347
|
+
};
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
return {
|
|
1350
|
+
name,
|
|
1351
|
+
ok: false,
|
|
1352
|
+
reason: e?.message || String(e)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function deployAllProcesses(group) {
|
|
1357
|
+
const processes = getAllProcesses().filter((proc) => !isInternalProcess(proc.name)).filter((proc) => !group || proc.group === group);
|
|
1358
|
+
const seen = new Set;
|
|
1359
|
+
const results = [];
|
|
1360
|
+
for (const proc of processes) {
|
|
1361
|
+
if (seen.has(proc.name))
|
|
1362
|
+
continue;
|
|
1363
|
+
seen.add(proc.name);
|
|
1364
|
+
results.push(await deployProcess(proc.name));
|
|
1365
|
+
}
|
|
1366
|
+
return results;
|
|
1367
|
+
}
|
|
1368
|
+
export {
|
|
1369
|
+
formatDeployToolError,
|
|
1370
|
+
detectPackageManager,
|
|
1371
|
+
deployProcess,
|
|
1372
|
+
deployAllProcesses
|
|
1373
|
+
};
|