bgrun 3.12.12 → 3.12.14
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/lib/runtime.ts +49 -0
- package/package.json +2 -17
- package/src/api.ts +0 -116
- package/src/build.ts +0 -31
- 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/src/db.ts
DELETED
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
import { Database, z } from "sqlite-zod-orm";
|
|
2
|
-
import { getHomeDir, ensureDir } from "./platform";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
import { sleep } from "bun";
|
|
5
|
-
import { existsSync, copyFileSync } from "fs";
|
|
6
|
-
|
|
7
|
-
// =============================================================================
|
|
8
|
-
// SCHEMA (inline — single table, no need for a separate file)
|
|
9
|
-
// =============================================================================
|
|
10
|
-
|
|
11
|
-
export const ProcessSchema = z.object({
|
|
12
|
-
pid: z.number(),
|
|
13
|
-
workdir: z.string(),
|
|
14
|
-
command: z.string(),
|
|
15
|
-
name: z.string(),
|
|
16
|
-
env: z.string(),
|
|
17
|
-
configPath: z.string().default(''),
|
|
18
|
-
stdout_path: z.string(),
|
|
19
|
-
stderr_path: z.string(),
|
|
20
|
-
timestamp: z.string().default(() => new Date().toISOString()),
|
|
21
|
-
group: z.string().default(''),
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export type Process = z.infer<typeof ProcessSchema> & { id: number };
|
|
25
|
-
|
|
26
|
-
export const TemplateSchema = z.object({
|
|
27
|
-
name: z.string(),
|
|
28
|
-
command: z.string(),
|
|
29
|
-
workdir: z.string().default(''),
|
|
30
|
-
env: z.string().default(''),
|
|
31
|
-
group: z.string().default(''),
|
|
32
|
-
created_at: z.string().default(() => new Date().toISOString()),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export type Template = z.infer<typeof TemplateSchema> & { id: number };
|
|
36
|
-
|
|
37
|
-
export const HistorySchema = z.object({
|
|
38
|
-
process_name: z.string(),
|
|
39
|
-
event: z.string(), // 'start', 'stop', 'restart', 'crash', 'guard_on', 'guard_off'
|
|
40
|
-
pid: z.number().optional(),
|
|
41
|
-
timestamp: z.string().default(() => new Date().toISOString()),
|
|
42
|
-
metadata: z.string().default(''), // JSON string for extra info
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
export type History = z.infer<typeof HistorySchema> & { id: number };
|
|
46
|
-
|
|
47
|
-
export const DependencySchema = z.object({
|
|
48
|
-
process_name: z.string(), // the process that has the dependency
|
|
49
|
-
depends_on: z.string(), // the process it depends on
|
|
50
|
-
created_at: z.string().default(() => new Date().toISOString()),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export type Dependency = z.infer<typeof DependencySchema> & { id: number };
|
|
54
|
-
|
|
55
|
-
// =============================================================================
|
|
56
|
-
// DATABASE INITIALIZATION
|
|
57
|
-
// =============================================================================
|
|
58
|
-
|
|
59
|
-
const homePath = getHomeDir();
|
|
60
|
-
const bgrDir = join(homePath, ".bgr");
|
|
61
|
-
ensureDir(bgrDir);
|
|
62
|
-
|
|
63
|
-
// DB filename: configurable via BGRUN_DB env, default "bgrun.sqlite"
|
|
64
|
-
const dbFilename = process.env.BGRUN_DB ?? "bgrun.sqlite";
|
|
65
|
-
export const dbPath = join(bgrDir, dbFilename);
|
|
66
|
-
export const bgrHome = bgrDir;
|
|
67
|
-
|
|
68
|
-
function shouldAutoMigrateLegacyDb() {
|
|
69
|
-
const raw = (process.env.BGRUN_DISABLE_LEGACY_MIGRATION || '').trim().toLowerCase();
|
|
70
|
-
return !(raw === '1' || raw === 'true' || raw === 'yes');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Auto-migration: if new DB doesn't exist but old one does, copy it over
|
|
74
|
-
const legacyDbPath = join(bgrDir, "bgr_v2.sqlite");
|
|
75
|
-
if (shouldAutoMigrateLegacyDb() && !existsSync(dbPath) && existsSync(legacyDbPath)) {
|
|
76
|
-
try {
|
|
77
|
-
copyFileSync(legacyDbPath, dbPath);
|
|
78
|
-
console.log(`[bgrun] Migrated database: ${legacyDbPath} → ${dbPath}`);
|
|
79
|
-
} catch (e) {
|
|
80
|
-
// Migration failed — start fresh
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const db = new Database(dbPath, {
|
|
85
|
-
process: ProcessSchema,
|
|
86
|
-
template: TemplateSchema,
|
|
87
|
-
history: HistorySchema,
|
|
88
|
-
dependency: DependencySchema,
|
|
89
|
-
}, {
|
|
90
|
-
indexes: {
|
|
91
|
-
process: ['name', 'timestamp', 'pid'],
|
|
92
|
-
template: ['name'],
|
|
93
|
-
history: ['process_name', 'timestamp'],
|
|
94
|
-
dependency: ['process_name', 'depends_on'],
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// =============================================================================
|
|
99
|
-
// QUERY FUNCTIONS
|
|
100
|
-
// =============================================================================
|
|
101
|
-
|
|
102
|
-
export function getProcess(name: string) {
|
|
103
|
-
return db.process.select()
|
|
104
|
-
.where({ name })
|
|
105
|
-
.orderBy('timestamp', 'desc')
|
|
106
|
-
.limit(1)
|
|
107
|
-
.get() || null;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function getAllProcesses() {
|
|
111
|
-
return db.process.select().all();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// =============================================================================
|
|
115
|
-
// MUTATION FUNCTIONS
|
|
116
|
-
// =============================================================================
|
|
117
|
-
|
|
118
|
-
export function insertProcess(data: {
|
|
119
|
-
pid: number;
|
|
120
|
-
workdir: string;
|
|
121
|
-
command: string;
|
|
122
|
-
name: string;
|
|
123
|
-
env: string;
|
|
124
|
-
configPath: string;
|
|
125
|
-
stdout_path: string;
|
|
126
|
-
stderr_path: string;
|
|
127
|
-
}) {
|
|
128
|
-
return db.process.insert({
|
|
129
|
-
...data,
|
|
130
|
-
timestamp: new Date().toISOString(),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function removeProcess(pid: number) {
|
|
135
|
-
const matches = db.process.select().where({ pid }).all();
|
|
136
|
-
for (const p of matches) {
|
|
137
|
-
db.process.delete(p.id);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function removeProcessByName(name: string) {
|
|
142
|
-
const matches = db.process.select().where({ name }).all();
|
|
143
|
-
for (const p of matches) {
|
|
144
|
-
db.process.delete(p.id);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Update the stored PID for a process (used by PID reconciliation) */
|
|
149
|
-
export function updateProcessPid(name: string, newPid: number) {
|
|
150
|
-
const proc = db.process.select().where({ name }).limit(1).get();
|
|
151
|
-
if (proc) {
|
|
152
|
-
db.process.update(proc.id, { pid: newPid });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function removeAllProcesses() {
|
|
157
|
-
const all = db.process.select().all();
|
|
158
|
-
for (const p of all) {
|
|
159
|
-
db.process.delete(p.id);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Update the stored env JSON for a process (used by guard toggle) */
|
|
164
|
-
export function updateProcessEnv(name: string, envJson: string) {
|
|
165
|
-
const proc = db.process.select().where({ name }).limit(1).get();
|
|
166
|
-
if (proc) {
|
|
167
|
-
db.process.update(proc.id, { env: envJson });
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// =============================================================================
|
|
172
|
-
// TEMPLATE FUNCTIONS
|
|
173
|
-
// =============================================================================
|
|
174
|
-
|
|
175
|
-
export function getAllTemplates() {
|
|
176
|
-
return db.template.select().all();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function getTemplate(name: string) {
|
|
180
|
-
return db.template.select().where({ name }).limit(1).get() || null;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function saveTemplate(data: {
|
|
184
|
-
name: string;
|
|
185
|
-
command: string;
|
|
186
|
-
workdir?: string;
|
|
187
|
-
env?: string;
|
|
188
|
-
group?: string;
|
|
189
|
-
}) {
|
|
190
|
-
const existing = db.template.select().where({ name: data.name }).limit(1).get();
|
|
191
|
-
if (existing) {
|
|
192
|
-
db.template.update(existing.id, {
|
|
193
|
-
command: data.command,
|
|
194
|
-
workdir: data.workdir || '',
|
|
195
|
-
env: data.env || '',
|
|
196
|
-
group: data.group || '',
|
|
197
|
-
});
|
|
198
|
-
} else {
|
|
199
|
-
db.template.insert({
|
|
200
|
-
name: data.name,
|
|
201
|
-
command: data.command,
|
|
202
|
-
workdir: data.workdir || '',
|
|
203
|
-
env: data.env || '',
|
|
204
|
-
group: data.group || '',
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function deleteTemplate(name: string) {
|
|
210
|
-
const tmpl = db.template.select().where({ name }).limit(1).get();
|
|
211
|
-
if (tmpl) {
|
|
212
|
-
db.template.delete(tmpl.id);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// =============================================================================
|
|
217
|
-
// HISTORY FUNCTIONS
|
|
218
|
-
// =============================================================================
|
|
219
|
-
|
|
220
|
-
export function getProcessHistory(name: string, limit = 50) {
|
|
221
|
-
return db.history.select()
|
|
222
|
-
.where({ process_name: name })
|
|
223
|
-
.orderBy('timestamp', 'desc')
|
|
224
|
-
.limit(limit)
|
|
225
|
-
.all();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function addHistoryEntry(processName: string, event: string, pid?: number, metadata = {}) {
|
|
229
|
-
return db.history.insert({
|
|
230
|
-
process_name: processName,
|
|
231
|
-
event,
|
|
232
|
-
pid,
|
|
233
|
-
metadata: JSON.stringify(metadata),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function getRecentHistory(limit = 100) {
|
|
238
|
-
return db.history.select()
|
|
239
|
-
.orderBy('timestamp', 'desc')
|
|
240
|
-
.limit(limit)
|
|
241
|
-
.all();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function clearOldHistory(daysToKeep = 30) {
|
|
245
|
-
const cutoff = new Date();
|
|
246
|
-
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
247
|
-
const cutoffStr = cutoff.toISOString();
|
|
248
|
-
|
|
249
|
-
const oldEntries = db.history.select()
|
|
250
|
-
.where('timestamp', '<', cutoffStr)
|
|
251
|
-
.all();
|
|
252
|
-
|
|
253
|
-
for (const entry of oldEntries) {
|
|
254
|
-
db.history.delete(entry.id);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return oldEntries.length;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// =============================================================================
|
|
261
|
-
// DEPENDENCY FUNCTIONS
|
|
262
|
-
// =============================================================================
|
|
263
|
-
|
|
264
|
-
/** Get all dependencies for a process */
|
|
265
|
-
export function getDependencies(processName: string): string[] {
|
|
266
|
-
return db.dependency.select()
|
|
267
|
-
.where({ process_name: processName })
|
|
268
|
-
.all()
|
|
269
|
-
.map(d => d.depends_on);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Get all processes that depend on a given process */
|
|
273
|
-
export function getDependents(processName: string): string[] {
|
|
274
|
-
return db.dependency.select()
|
|
275
|
-
.where({ depends_on: processName })
|
|
276
|
-
.all()
|
|
277
|
-
.map(d => d.process_name);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/** Get the full dependency graph: { processName -> [dependsOn...] } */
|
|
281
|
-
export function getDependencyGraph(): Record<string, string[]> {
|
|
282
|
-
const all = db.dependency.select().all();
|
|
283
|
-
const graph: Record<string, string[]> = {};
|
|
284
|
-
for (const dep of all) {
|
|
285
|
-
if (!graph[dep.process_name]) graph[dep.process_name] = [];
|
|
286
|
-
graph[dep.process_name].push(dep.depends_on);
|
|
287
|
-
}
|
|
288
|
-
return graph;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** Add a dependency (process_name depends on depends_on) */
|
|
292
|
-
export function addDependency(processName: string, dependsOn: string): boolean {
|
|
293
|
-
// Prevent self-dependency
|
|
294
|
-
if (processName === dependsOn) return false;
|
|
295
|
-
|
|
296
|
-
// Prevent duplicates
|
|
297
|
-
const existing = db.dependency.select()
|
|
298
|
-
.where({ process_name: processName, depends_on: dependsOn })
|
|
299
|
-
.limit(1)
|
|
300
|
-
.get();
|
|
301
|
-
if (existing) return false;
|
|
302
|
-
|
|
303
|
-
// Prevent circular dependencies
|
|
304
|
-
if (wouldCreateCycle(processName, dependsOn)) return false;
|
|
305
|
-
|
|
306
|
-
db.dependency.insert({ process_name: processName, depends_on: dependsOn });
|
|
307
|
-
return true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/** Remove a dependency */
|
|
311
|
-
export function removeDependency(processName: string, dependsOn: string) {
|
|
312
|
-
const matches = db.dependency.select()
|
|
313
|
-
.where({ process_name: processName, depends_on: dependsOn })
|
|
314
|
-
.all();
|
|
315
|
-
for (const dep of matches) {
|
|
316
|
-
db.dependency.delete(dep.id);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/** Remove all dependencies for a process */
|
|
321
|
-
export function removeAllDependencies(processName: string) {
|
|
322
|
-
const matches = db.dependency.select()
|
|
323
|
-
.where({ process_name: processName })
|
|
324
|
-
.all();
|
|
325
|
-
for (const dep of matches) {
|
|
326
|
-
db.dependency.delete(dep.id);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/** Check if adding processName -> dependsOn would create a cycle */
|
|
331
|
-
function wouldCreateCycle(processName: string, dependsOn: string): boolean {
|
|
332
|
-
const graph = getDependencyGraph();
|
|
333
|
-
// Add the proposed edge temporarily
|
|
334
|
-
if (!graph[processName]) graph[processName] = [];
|
|
335
|
-
graph[processName].push(dependsOn);
|
|
336
|
-
|
|
337
|
-
// DFS from dependsOn — if we can reach processName, it's a cycle
|
|
338
|
-
const visited = new Set<string>();
|
|
339
|
-
const stack = [dependsOn];
|
|
340
|
-
while (stack.length > 0) {
|
|
341
|
-
const current = stack.pop()!;
|
|
342
|
-
if (current === processName) return true;
|
|
343
|
-
if (visited.has(current)) continue;
|
|
344
|
-
visited.add(current);
|
|
345
|
-
for (const dep of (graph[current] || [])) {
|
|
346
|
-
stack.push(dep);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** Get topological start order (processes with no deps first) */
|
|
353
|
-
export function getStartOrder(): string[] {
|
|
354
|
-
const graph = getDependencyGraph();
|
|
355
|
-
const allProcesses = getAllProcesses().map(p => p.name);
|
|
356
|
-
const allNames = new Set(allProcesses);
|
|
357
|
-
|
|
358
|
-
// Build in-degree map
|
|
359
|
-
const inDegree: Record<string, number> = {};
|
|
360
|
-
for (const name of allNames) inDegree[name] = 0;
|
|
361
|
-
for (const [proc, deps] of Object.entries(graph)) {
|
|
362
|
-
for (const dep of deps) {
|
|
363
|
-
if (allNames.has(dep)) {
|
|
364
|
-
inDegree[proc] = (inDegree[proc] || 0) + 1;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Kahn's algorithm
|
|
370
|
-
const queue: string[] = [];
|
|
371
|
-
for (const name of allNames) {
|
|
372
|
-
if ((inDegree[name] || 0) === 0) queue.push(name);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const order: string[] = [];
|
|
376
|
-
while (queue.length > 0) {
|
|
377
|
-
queue.sort(); // stable alphabetical within same level
|
|
378
|
-
const current = queue.shift()!;
|
|
379
|
-
order.push(current);
|
|
380
|
-
// Find processes that depend on current
|
|
381
|
-
for (const [proc, deps] of Object.entries(graph)) {
|
|
382
|
-
if (deps.includes(current) && allNames.has(proc)) {
|
|
383
|
-
inDegree[proc]--;
|
|
384
|
-
if (inDegree[proc] === 0) queue.push(proc);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return order;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// =============================================================================
|
|
393
|
-
// DEBUG / INFO
|
|
394
|
-
// =============================================================================
|
|
395
|
-
|
|
396
|
-
export function getDbInfo() {
|
|
397
|
-
return {
|
|
398
|
-
dbPath,
|
|
399
|
-
bgrHome,
|
|
400
|
-
dbFilename,
|
|
401
|
-
exists: existsSync(dbPath),
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// =============================================================================
|
|
406
|
-
// UTILITIES
|
|
407
|
-
// =============================================================================
|
|
408
|
-
|
|
409
|
-
export async function retryDatabaseOperation<T>(operation: () => T, maxRetries = 5, delay = 100): Promise<T> {
|
|
410
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
411
|
-
try {
|
|
412
|
-
return operation();
|
|
413
|
-
} catch (err: any) {
|
|
414
|
-
if (err?.code === 'SQLITE_BUSY' && attempt < maxRetries) {
|
|
415
|
-
await sleep(delay * attempt);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
throw err;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
throw new Error('Max retries reached for database operation');
|
|
422
|
-
}
|
package/src/deploy.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { getProcess, getAllProcesses, addHistoryEntry } from './db';
|
|
2
|
-
import { handleRun } from './commands/run';
|
|
3
|
-
import { $ } from 'bun';
|
|
4
|
-
|
|
5
|
-
export interface DeployResult {
|
|
6
|
-
name: string;
|
|
7
|
-
ok: boolean;
|
|
8
|
-
skipped?: boolean;
|
|
9
|
-
reason?: string;
|
|
10
|
-
pullOutput?: string;
|
|
11
|
-
installOutput?: string;
|
|
12
|
-
packageManager?: PackageManager;
|
|
13
|
-
installCommand?: string;
|
|
14
|
-
installAttempted?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type PackageManager = 'bun' | 'pnpm' | 'yarn' | 'npm' | null;
|
|
18
|
-
|
|
19
|
-
export function formatDeployToolError(manager: Exclude<PackageManager, null>, error: unknown): string {
|
|
20
|
-
const raw = error instanceof Error ? error.message : String(error ?? 'Unknown error');
|
|
21
|
-
const lower = raw.toLowerCase();
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
lower.includes('command not found') ||
|
|
25
|
-
lower.includes('not recognized as an internal or external command') ||
|
|
26
|
-
lower.includes('executable not found') ||
|
|
27
|
-
lower.includes('no such file or directory')
|
|
28
|
-
) {
|
|
29
|
-
return `Deploy requires '${manager}', but it is not installed or not available on PATH.`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return `Dependency install failed with ${manager}: ${raw}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isInternalProcess(name: string): boolean {
|
|
36
|
-
return name === 'bgr-dashboard' || name === 'bgr-guard';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function pathExists(path: string): Promise<boolean> {
|
|
40
|
-
return await Bun.file(path).exists();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function isGitRepo(dir: string): Promise<boolean> {
|
|
44
|
-
return await pathExists(`${dir}/.git`) || await pathExists(`${dir}/.git/HEAD`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function detectPackageManager(dir: string): Promise<PackageManager> {
|
|
48
|
-
const hasPackageJson = await pathExists(`${dir}/package.json`);
|
|
49
|
-
if (!hasPackageJson) return null;
|
|
50
|
-
|
|
51
|
-
if (await pathExists(`${dir}/bun.lock`) || await pathExists(`${dir}/bun.lockb`)) return 'bun';
|
|
52
|
-
if (await pathExists(`${dir}/pnpm-lock.yaml`)) return 'pnpm';
|
|
53
|
-
if (await pathExists(`${dir}/yarn.lock`)) return 'yarn';
|
|
54
|
-
if (await pathExists(`${dir}/package-lock.json`) || await pathExists(`${dir}/npm-shrinkwrap.json`)) return 'npm';
|
|
55
|
-
|
|
56
|
-
return 'bun';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function getInstallCommand(manager: Exclude<PackageManager, null>): string {
|
|
60
|
-
switch (manager) {
|
|
61
|
-
case 'bun': return 'bun install';
|
|
62
|
-
case 'pnpm': return 'pnpm install --frozen-lockfile';
|
|
63
|
-
case 'yarn': return 'yarn install --frozen-lockfile';
|
|
64
|
-
case 'npm': return 'npm ci';
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function installDependencies(dir: string): Promise<{ manager: PackageManager; output: string; command: string }> {
|
|
69
|
-
const manager = await detectPackageManager(dir);
|
|
70
|
-
if (!manager) return { manager: null, output: '', command: '' };
|
|
71
|
-
|
|
72
|
-
$.cwd(dir);
|
|
73
|
-
const command = getInstallCommand(manager);
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
switch (manager) {
|
|
77
|
-
case 'bun':
|
|
78
|
-
return { manager, command, output: (await $`bun install`.text()).trim() };
|
|
79
|
-
case 'pnpm':
|
|
80
|
-
return { manager, command, output: (await $`pnpm install --frozen-lockfile`.text()).trim() };
|
|
81
|
-
case 'yarn':
|
|
82
|
-
return { manager, command, output: (await $`yarn install --frozen-lockfile`.text()).trim() };
|
|
83
|
-
case 'npm':
|
|
84
|
-
return { manager, command, output: (await $`npm ci`.text()).trim() };
|
|
85
|
-
default:
|
|
86
|
-
return { manager: null, output: '', command: '' };
|
|
87
|
-
}
|
|
88
|
-
} catch (error) {
|
|
89
|
-
throw new Error(formatDeployToolError(manager, error));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function deployProcess(name: string): Promise<DeployResult> {
|
|
94
|
-
const proc = getProcess(name);
|
|
95
|
-
if (!proc) {
|
|
96
|
-
return { name, ok: false, reason: `Process '${name}' not found` };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (isInternalProcess(proc.name)) {
|
|
100
|
-
return { name, ok: false, skipped: true, reason: 'Internal bgrun process skipped' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const dir = proc.workdir;
|
|
104
|
-
if (!(await isGitRepo(dir))) {
|
|
105
|
-
return { name, ok: false, skipped: true, reason: `'${dir}' is not a git repository` };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
$.cwd(dir);
|
|
110
|
-
|
|
111
|
-
const pullOutput = (await $`git pull`.text()).trim();
|
|
112
|
-
|
|
113
|
-
const install = await installDependencies(dir);
|
|
114
|
-
const installOutput = install.output;
|
|
115
|
-
|
|
116
|
-
await handleRun({
|
|
117
|
-
action: 'run',
|
|
118
|
-
name,
|
|
119
|
-
force: true,
|
|
120
|
-
remoteName: '',
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
addHistoryEntry(name, 'deploy', proc.pid, {
|
|
124
|
-
directory: dir,
|
|
125
|
-
installed: Boolean(install.manager),
|
|
126
|
-
packageManager: install.manager,
|
|
127
|
-
installCommand: install.command,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
name,
|
|
132
|
-
ok: true,
|
|
133
|
-
pullOutput,
|
|
134
|
-
installOutput,
|
|
135
|
-
packageManager: install.manager,
|
|
136
|
-
installCommand: install.command,
|
|
137
|
-
installAttempted: Boolean(install.manager),
|
|
138
|
-
};
|
|
139
|
-
} catch (e: any) {
|
|
140
|
-
return {
|
|
141
|
-
name,
|
|
142
|
-
ok: false,
|
|
143
|
-
reason: e?.message || String(e),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export async function deployAllProcesses(group?: string): Promise<DeployResult[]> {
|
|
149
|
-
const processes = getAllProcesses()
|
|
150
|
-
.filter(proc => !isInternalProcess(proc.name))
|
|
151
|
-
.filter(proc => !group || proc.group === group);
|
|
152
|
-
|
|
153
|
-
const seen = new Set<string>();
|
|
154
|
-
const results: DeployResult[] = [];
|
|
155
|
-
|
|
156
|
-
for (const proc of processes) {
|
|
157
|
-
if (seen.has(proc.name)) continue;
|
|
158
|
-
seen.add(proc.name);
|
|
159
|
-
results.push(await deployProcess(proc.name));
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return results;
|
|
163
|
-
}
|
package/src/deps.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Process Dependency Graph
|
|
3
|
-
*
|
|
4
|
-
* Defines and resolves process startup dependencies.
|
|
5
|
-
* Dependencies are stored in process env as BGR_DEPENDS_ON=name1,name2
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Topological sort for startup order
|
|
9
|
-
* - Cycle detection
|
|
10
|
-
* - Dependency graph as adjacency list
|
|
11
|
-
* - Auto-start dependencies on process run
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { getAllProcesses, getProcess } from './db'
|
|
15
|
-
import { isProcessRunning } from './platform'
|
|
16
|
-
import { parseEnvString } from './utils'
|
|
17
|
-
|
|
18
|
-
export interface DepNode {
|
|
19
|
-
name: string
|
|
20
|
-
dependsOn: string[] // processes this depends ON (must start first)
|
|
21
|
-
dependedBy: string[] // processes that depend on THIS
|
|
22
|
-
running: boolean
|
|
23
|
-
pid: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DepGraph {
|
|
27
|
-
nodes: DepNode[]
|
|
28
|
-
order: string[] // topological sort (startup order)
|
|
29
|
-
hasCycle: boolean
|
|
30
|
-
cycleNodes?: string[] // nodes involved in cycle
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Parse BGR_DEPENDS_ON from a process env string */
|
|
34
|
-
export function getDependencies(envStr: string): string[] {
|
|
35
|
-
const env = parseEnvString(envStr)
|
|
36
|
-
const raw = env.BGR_DEPENDS_ON || ''
|
|
37
|
-
return raw.split(',').map(s => s.trim()).filter(Boolean)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Build the full dependency graph from all registered processes */
|
|
41
|
-
export async function buildDepGraph(): Promise<DepGraph> {
|
|
42
|
-
const processes = getAllProcesses()
|
|
43
|
-
const nodeMap = new Map<string, DepNode>()
|
|
44
|
-
|
|
45
|
-
// Phase 1: Create all nodes
|
|
46
|
-
for (const proc of processes) {
|
|
47
|
-
const deps = getDependencies(proc.env)
|
|
48
|
-
const alive = await isProcessRunning(proc.pid, proc.command)
|
|
49
|
-
nodeMap.set(proc.name, {
|
|
50
|
-
name: proc.name,
|
|
51
|
-
dependsOn: deps,
|
|
52
|
-
dependedBy: [],
|
|
53
|
-
running: alive,
|
|
54
|
-
pid: proc.pid,
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Phase 2: Build reverse edges (dependedBy)
|
|
59
|
-
for (const node of nodeMap.values()) {
|
|
60
|
-
for (const dep of node.dependsOn) {
|
|
61
|
-
const depNode = nodeMap.get(dep)
|
|
62
|
-
if (depNode) {
|
|
63
|
-
depNode.dependedBy.push(node.name)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Phase 3: Topological sort (Kahn's algorithm)
|
|
69
|
-
const inDegree = new Map<string, number>()
|
|
70
|
-
for (const node of nodeMap.values()) {
|
|
71
|
-
inDegree.set(node.name, node.dependsOn.filter(d => nodeMap.has(d)).length)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const queue: string[] = []
|
|
75
|
-
for (const [name, degree] of inDegree) {
|
|
76
|
-
if (degree === 0) queue.push(name)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const order: string[] = []
|
|
80
|
-
while (queue.length > 0) {
|
|
81
|
-
const current = queue.shift()!
|
|
82
|
-
order.push(current)
|
|
83
|
-
|
|
84
|
-
const node = nodeMap.get(current)!
|
|
85
|
-
for (const dependent of node.dependedBy) {
|
|
86
|
-
const newDegree = (inDegree.get(dependent) || 0) - 1
|
|
87
|
-
inDegree.set(dependent, newDegree)
|
|
88
|
-
if (newDegree === 0) queue.push(dependent)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const hasCycle = order.length < nodeMap.size
|
|
93
|
-
const cycleNodes = hasCycle
|
|
94
|
-
? [...nodeMap.keys()].filter(n => !order.includes(n))
|
|
95
|
-
: undefined
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
nodes: [...nodeMap.values()],
|
|
99
|
-
order,
|
|
100
|
-
hasCycle,
|
|
101
|
-
cycleNodes,
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Get unmet dependencies for a specific process */
|
|
106
|
-
export async function getUnmetDeps(name: string): Promise<string[]> {
|
|
107
|
-
const proc = getProcess(name)
|
|
108
|
-
if (!proc) return []
|
|
109
|
-
|
|
110
|
-
const deps = getDependencies(proc.env)
|
|
111
|
-
const unmet: string[] = []
|
|
112
|
-
|
|
113
|
-
for (const depName of deps) {
|
|
114
|
-
const depProc = getProcess(depName)
|
|
115
|
-
if (!depProc) {
|
|
116
|
-
unmet.push(depName) // dependency not even registered
|
|
117
|
-
continue
|
|
118
|
-
}
|
|
119
|
-
const alive = await isProcessRunning(depProc.pid, depProc.command)
|
|
120
|
-
if (!alive) {
|
|
121
|
-
unmet.push(depName)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return unmet
|
|
126
|
-
}
|