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