bgrun 3.12.1 → 3.12.3
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/dashboard/app/api/check-port/route.ts +35 -0
- package/dashboard/app/api/dependencies/route.ts +40 -0
- package/dashboard/app/api/deploy/[name]/route.ts +6 -41
- package/dashboard/app/api/deploy-all/route.ts +25 -0
- package/dashboard/app/api/next-port/route.ts +32 -0
- package/dashboard/app/api/start/route.ts +6 -0
- package/dashboard/app/globals.css +1313 -108
- package/dashboard/app/page.client.tsx +1576 -8
- package/dashboard/app/page.tsx +194 -5
- package/dist/index.js +143 -25
- package/package.json +1 -1
- package/src/bgrun.test.ts +204 -0
- package/src/db.ts +142 -0
- package/src/deploy.ts +163 -0
- package/src/index.ts +9 -0
- package/src/platform.ts +29 -14
package/dashboard/app/page.tsx
CHANGED
|
@@ -12,20 +12,20 @@ export default function DashboardPage() {
|
|
|
12
12
|
<div className="toast-container" id="toast-container"></div>
|
|
13
13
|
|
|
14
14
|
{/* Stats Grid */}
|
|
15
|
-
<div className="stats-grid">
|
|
16
|
-
<div className="stat-card">
|
|
15
|
+
<div className="stats-grid" id="stats-grid">
|
|
16
|
+
<div className="stat-card stat-clickable" data-stat-filter="all">
|
|
17
17
|
<div className="stat-label">Total Processes</div>
|
|
18
18
|
<div className="stat-value" id="total-count">–</div>
|
|
19
19
|
</div>
|
|
20
|
-
<div className="stat-card running">
|
|
20
|
+
<div className="stat-card running stat-clickable" data-stat-filter="running">
|
|
21
21
|
<div className="stat-label">Running</div>
|
|
22
22
|
<div className="stat-value" id="running-count">–</div>
|
|
23
23
|
</div>
|
|
24
|
-
<div className="stat-card stopped">
|
|
24
|
+
<div className="stat-card stopped stat-clickable" data-stat-filter="stopped">
|
|
25
25
|
<div className="stat-label">Stopped</div>
|
|
26
26
|
<div className="stat-value" id="stopped-count">–</div>
|
|
27
27
|
</div>
|
|
28
|
-
<div className="stat-card guarded">
|
|
28
|
+
<div className="stat-card guarded stat-clickable" data-stat-filter="guarded">
|
|
29
29
|
<div className="stat-label">Guarded</div>
|
|
30
30
|
<div className="stat-value" id="guarded-count">–</div>
|
|
31
31
|
</div>
|
|
@@ -37,6 +37,14 @@ export default function DashboardPage() {
|
|
|
37
37
|
<div className="stat-label">Guard Restarts</div>
|
|
38
38
|
<div className="stat-value" id="restarts-count">0</div>
|
|
39
39
|
</div>
|
|
40
|
+
<div className="stat-card uptime">
|
|
41
|
+
<div className="stat-label">Longest Uptime</div>
|
|
42
|
+
<div className="stat-value" id="uptime-longest">–</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="stat-card uptime-total">
|
|
45
|
+
<div className="stat-label">Total Uptime</div>
|
|
46
|
+
<div className="stat-value" id="uptime-total">–</div>
|
|
47
|
+
</div>
|
|
40
48
|
</div>
|
|
41
49
|
|
|
42
50
|
{/* Guard Activity Feed */}
|
|
@@ -65,9 +73,14 @@ export default function DashboardPage() {
|
|
|
65
73
|
<span className="search-count" id="search-count" style={{ display: 'none' }}></span>
|
|
66
74
|
<span className="search-shortcut">/</span>
|
|
67
75
|
</div>
|
|
76
|
+
<span className="stat-filter-badge" id="stat-filter-badge" style={{ display: 'none' }}>
|
|
77
|
+
<span id="stat-filter-badge-label"></span>
|
|
78
|
+
<button className="stat-filter-badge-clear" id="stat-filter-badge-clear" type="button" title="Clear status filter">✕</button>
|
|
79
|
+
</span>
|
|
68
80
|
<select className="group-filter" id="group-filter">
|
|
69
81
|
<option value="">All Groups</option>
|
|
70
82
|
</select>
|
|
83
|
+
<div className="deploy-preset-scopes" id="deploy-preset-scopes"></div>
|
|
71
84
|
</div>
|
|
72
85
|
<div className="toolbar-right">
|
|
73
86
|
<button className="btn btn-ghost btn-icon" id="refresh-btn" title="Refresh">
|
|
@@ -82,10 +95,40 @@ export default function DashboardPage() {
|
|
|
82
95
|
</svg>
|
|
83
96
|
<span id="guard-all-label">Guard All</span>
|
|
84
97
|
</button>
|
|
98
|
+
<button className="btn btn-ghost" id="deploy-all-btn" title="Git pull + restart all deployable processes">
|
|
99
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
100
|
+
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z" />
|
|
101
|
+
<path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z" />
|
|
102
|
+
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
|
|
103
|
+
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
|
|
104
|
+
</svg>
|
|
105
|
+
<span id="deploy-all-label">Deploy All</span>
|
|
106
|
+
</button>
|
|
107
|
+
<div className="deploy-controls">
|
|
108
|
+
<label className="deploy-concurrency-wrap" title="Bulk deploy concurrency (saved per group)">
|
|
109
|
+
<span className="deploy-concurrency-label">Deploy</span>
|
|
110
|
+
<select className="deploy-concurrency-select" id="deploy-concurrency-select">
|
|
111
|
+
<option value="1">1×</option>
|
|
112
|
+
<option value="2">2×</option>
|
|
113
|
+
<option value="3">3×</option>
|
|
114
|
+
<option value="4">4×</option>
|
|
115
|
+
</select>
|
|
116
|
+
</label>
|
|
117
|
+
<span className="deploy-preset-source" id="deploy-preset-source" title="Current deploy preset source">default</span>
|
|
118
|
+
<button className="btn btn-ghost btn-icon" id="deploy-preset-reset-btn" title="Reset saved deploy preset for current group">
|
|
119
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
120
|
+
<polyline points="1 4 1 10 7 10" />
|
|
121
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
122
|
+
</svg>
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
85
125
|
<span className="guard-sentinel-pill" id="guard-sentinel-pill" title="Standalone guard process status">
|
|
86
126
|
<span className="guard-sentinel-dot" id="guard-sentinel-dot" />
|
|
87
127
|
<span id="guard-sentinel-label">Guard: –</span>
|
|
88
128
|
</span>
|
|
129
|
+
<button className="btn btn-ghost btn-icon" id="theme-toggle-btn" title="Toggle light/dark theme">
|
|
130
|
+
<span id="theme-toggle-icon" style={{ fontSize: '0.85rem' }}>🌙</span>
|
|
131
|
+
</button>
|
|
89
132
|
<button className="btn btn-ghost btn-icon" id="shortcuts-btn" title="Keyboard Shortcuts (?)">
|
|
90
133
|
<span style={{ fontSize: '0.85rem', fontWeight: '700' }}>?</span>
|
|
91
134
|
</button>
|
|
@@ -96,6 +139,17 @@ export default function DashboardPage() {
|
|
|
96
139
|
</svg>
|
|
97
140
|
Templates
|
|
98
141
|
</button>
|
|
142
|
+
<button className="btn btn-ghost" id="deps-btn" title="Process Dependencies">
|
|
143
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
144
|
+
<circle cx="6" cy="6" r="3" />
|
|
145
|
+
<circle cx="18" cy="6" r="3" />
|
|
146
|
+
<circle cx="18" cy="18" r="3" />
|
|
147
|
+
<line x1="9" y1="6" x2="15" y2="6" />
|
|
148
|
+
<path d="M18 9v6" />
|
|
149
|
+
<path d="M9 6l6 9" />
|
|
150
|
+
</svg>
|
|
151
|
+
Deps
|
|
152
|
+
</button>
|
|
99
153
|
<button className="btn btn-ghost" id="history-btn">
|
|
100
154
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
101
155
|
<circle cx="12" cy="12" r="10" />
|
|
@@ -244,6 +298,19 @@ export default function DashboardPage() {
|
|
|
244
298
|
<label htmlFor="process-directory-input">Working Directory</label>
|
|
245
299
|
<input type="text" id="process-directory-input" placeholder="/path/to/project" />
|
|
246
300
|
</div>
|
|
301
|
+
<div className="form-group">
|
|
302
|
+
<label htmlFor="process-port-input">Port <span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>(optional)</span></label>
|
|
303
|
+
<div className="port-input-wrap">
|
|
304
|
+
<input type="number" id="process-port-input" placeholder="auto" min="1" max="65535" />
|
|
305
|
+
<button className="btn btn-ghost btn-sm" id="suggest-port-btn" type="button" title="Auto-assign next available port">Suggest</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="port-range-wrap">
|
|
308
|
+
<span className="port-range-label">Range</span>
|
|
309
|
+
<input type="number" id="port-range-min" className="port-range-input" placeholder="3001" min="1" max="65535" />
|
|
310
|
+
<span className="port-range-sep">–</span>
|
|
311
|
+
<input type="number" id="port-range-max" className="port-range-input" placeholder="4000" min="1" max="65535" />
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
247
314
|
</div>
|
|
248
315
|
<div className="modal-footer">
|
|
249
316
|
<button className="btn btn-ghost" id="modal-cancel-btn">Cancel</button>
|
|
@@ -253,6 +320,33 @@ export default function DashboardPage() {
|
|
|
253
320
|
</div>
|
|
254
321
|
|
|
255
322
|
{/* History Modal */}
|
|
323
|
+
{/* Dependencies Modal */}
|
|
324
|
+
<div className="modal-overlay" id="deps-modal">
|
|
325
|
+
<div className="modal modal-wide">
|
|
326
|
+
<div className="modal-header">
|
|
327
|
+
<h3>🔗 Process Dependencies</h3>
|
|
328
|
+
<button className="modal-close" id="deps-modal-close">✕</button>
|
|
329
|
+
</div>
|
|
330
|
+
<div className="modal-body">
|
|
331
|
+
<div className="deps-controls">
|
|
332
|
+
<select id="deps-process-select" className="history-select">
|
|
333
|
+
<option value="">Select process...</option>
|
|
334
|
+
</select>
|
|
335
|
+
<span className="deps-arrow">depends on →</span>
|
|
336
|
+
<select id="deps-target-select" className="history-select">
|
|
337
|
+
<option value="">Select dependency...</option>
|
|
338
|
+
</select>
|
|
339
|
+
<button className="btn btn-primary btn-sm" id="deps-add-btn">Add</button>
|
|
340
|
+
</div>
|
|
341
|
+
<div className="deps-graph-container" id="deps-graph-container">
|
|
342
|
+
<svg id="deps-graph-svg" width="100%" height="400"></svg>
|
|
343
|
+
</div>
|
|
344
|
+
<div className="deps-list" id="deps-list"></div>
|
|
345
|
+
<div className="deps-start-order" id="deps-start-order"></div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
256
350
|
<div className="modal-overlay" id="history-modal">
|
|
257
351
|
<div className="modal modal-wide">
|
|
258
352
|
<div className="modal-header">
|
|
@@ -269,9 +363,85 @@ export default function DashboardPage() {
|
|
|
269
363
|
<option value="start">Start</option>
|
|
270
364
|
<option value="stop">Stop</option>
|
|
271
365
|
<option value="restart">Restart</option>
|
|
366
|
+
<option value="deploy">Deploy</option>
|
|
272
367
|
<option value="guard_on">Guard On</option>
|
|
273
368
|
<option value="guard_off">Guard Off</option>
|
|
274
369
|
</select>
|
|
370
|
+
<input id="history-metadata-filter" className="history-select history-search" type="text" placeholder="Filter metadata (comma-separated)..." />
|
|
371
|
+
<label className="history-density-wrap" title="History row density">
|
|
372
|
+
<span className="history-density-label">Density</span>
|
|
373
|
+
<select id="history-density-select" className="history-select history-density-select">
|
|
374
|
+
<option value="cozy">Cozy</option>
|
|
375
|
+
<option value="compact">Compact</option>
|
|
376
|
+
</select>
|
|
377
|
+
</label>
|
|
378
|
+
<label className="history-shortcuts-toggle" title="Show quick-action affordances in History rows">
|
|
379
|
+
<input id="history-shortcuts-toggle" type="checkbox" />
|
|
380
|
+
<span>Shortcuts</span>
|
|
381
|
+
</label>
|
|
382
|
+
<label className="history-density-wrap" title="Default History details state">
|
|
383
|
+
<span className="history-density-label">Details</span>
|
|
384
|
+
<select id="history-details-default-select" className="history-select history-density-select">
|
|
385
|
+
<option value="collapsed">Collapsed</option>
|
|
386
|
+
<option value="expanded">Expanded</option>
|
|
387
|
+
</select>
|
|
388
|
+
</label>
|
|
389
|
+
<button className="btn btn-ghost btn-sm" id="history-clear-filters-btn" title="Clear all history filters">Clear</button>
|
|
390
|
+
</div>
|
|
391
|
+
<div className="history-hints-bar">
|
|
392
|
+
<div className="history-hints-bar-left">
|
|
393
|
+
<span className="history-hints-title">Keyboard shortcuts</span>
|
|
394
|
+
<div className="history-focus-controls">
|
|
395
|
+
<button className="history-focus-jump" id="history-focus-prev" type="button" title="Focus previous History row">←</button>
|
|
396
|
+
<span className="history-focus-status" id="history-focus-status">No row selected</span>
|
|
397
|
+
<button className="history-focus-jump" id="history-focus-next" type="button" title="Focus next History row">→</button>
|
|
398
|
+
</div>
|
|
399
|
+
<label className="history-auto-open-toggle" title="Automatically sync the process drawer while stepping through History rows">
|
|
400
|
+
<input id="history-auto-open-toggle" type="checkbox" />
|
|
401
|
+
<span>Auto-open</span>
|
|
402
|
+
</label>
|
|
403
|
+
<label className="history-hint-density-wrap" title="Auto-open behavior while stepping through History rows">
|
|
404
|
+
<span className="history-density-label">Scope</span>
|
|
405
|
+
<select id="history-focus-scope-select" className="history-select history-hint-density-select">
|
|
406
|
+
<option value="sync">Sync drawer</option>
|
|
407
|
+
<option value="inspect">Inspect</option>
|
|
408
|
+
</select>
|
|
409
|
+
</label>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="history-hints-actions">
|
|
412
|
+
<label className="history-hint-density-wrap" title="Keyboard hint density">
|
|
413
|
+
<span className="history-density-label">Hints</span>
|
|
414
|
+
<select id="history-hint-density-select" className="history-select history-hint-density-select">
|
|
415
|
+
<option value="full">Full</option>
|
|
416
|
+
<option value="compact">Compact</option>
|
|
417
|
+
</select>
|
|
418
|
+
</label>
|
|
419
|
+
<details className="history-hint-groups-menu">
|
|
420
|
+
<summary className="history-hints-toggle" title="Choose which History hint groups to show">Groups</summary>
|
|
421
|
+
<div className="history-hint-groups-panel">
|
|
422
|
+
<div className="history-hint-presets">
|
|
423
|
+
<button className="history-hint-preset" type="button" data-history-hint-preset="minimal">Minimal</button>
|
|
424
|
+
<button className="history-hint-preset" type="button" data-history-hint-preset="navigation">Navigation</button>
|
|
425
|
+
<button className="history-hint-preset" type="button" data-history-hint-preset="all">All</button>
|
|
426
|
+
<span className="history-hint-preset-state" id="history-hint-preset-state">Custom</span>
|
|
427
|
+
</div>
|
|
428
|
+
<label className="history-hint-group-option"><input id="history-hint-group-nav" type="checkbox" /> <span>Navigation</span></label>
|
|
429
|
+
<label className="history-hint-group-option"><input id="history-hint-group-open" type="checkbox" /> <span>Open</span></label>
|
|
430
|
+
<label className="history-hint-group-option"><input id="history-hint-group-filter" type="checkbox" /> <span>Filters</span></label>
|
|
431
|
+
<label className="history-hint-group-option"><input id="history-hint-group-details" type="checkbox" /> <span>Details</span></label>
|
|
432
|
+
<label className="history-hint-group-option"><input id="history-hint-group-close" type="checkbox" /> <span>Close</span></label>
|
|
433
|
+
</div>
|
|
434
|
+
</details>
|
|
435
|
+
<button className="history-hints-toggle" id="history-hints-toggle" type="button" title="Hide keyboard shortcut hints">Hide</button>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
<div className="history-keyboard-hints" id="history-keyboard-hints" aria-label="History keyboard shortcuts">
|
|
439
|
+
<span className="history-keyboard-hint" data-hint-group="nav"><kbd>↑</kbd><kbd>↓</kbd><span>Move</span></span>
|
|
440
|
+
<span className="history-keyboard-hint" data-hint-group="open"><kbd>Enter</kbd><span>Open</span></span>
|
|
441
|
+
<span className="history-keyboard-hint" data-hint-group="filter"><kbd>F</kbd><span>Process filter</span></span>
|
|
442
|
+
<span className="history-keyboard-hint" data-hint-group="filter"><kbd>E</kbd><span>Event filter</span></span>
|
|
443
|
+
<span className="history-keyboard-hint" data-hint-group="details"><kbd>Space</kbd><span>Toggle details</span></span>
|
|
444
|
+
<span className="history-keyboard-hint" data-hint-group="close"><kbd>Esc</kbd><span>Close</span></span>
|
|
275
445
|
</div>
|
|
276
446
|
<div className="history-list" id="history-list">
|
|
277
447
|
<div className="history-empty">No history yet</div>
|
|
@@ -324,6 +494,24 @@ export default function DashboardPage() {
|
|
|
324
494
|
</div>
|
|
325
495
|
</div>
|
|
326
496
|
|
|
497
|
+
{/* Deploy Results Modal */}
|
|
498
|
+
<div className="modal-overlay" id="deploy-results-modal">
|
|
499
|
+
<div className="modal modal-wide">
|
|
500
|
+
<div className="modal-header">
|
|
501
|
+
<h3>🚀 Deploy Results</h3>
|
|
502
|
+
<button className="modal-close" id="deploy-results-modal-close">✕</button>
|
|
503
|
+
</div>
|
|
504
|
+
<div className="modal-body">
|
|
505
|
+
<div className="deploy-results-summary" id="deploy-results-summary">
|
|
506
|
+
No deploy results yet
|
|
507
|
+
</div>
|
|
508
|
+
<div className="deploy-results-list" id="deploy-results-list">
|
|
509
|
+
<div className="history-empty">Run a bulk deploy to see detailed results</div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
327
515
|
{/* Keyboard Shortcuts Overlay */}
|
|
328
516
|
<div className="shortcuts-overlay" id="shortcuts-overlay">
|
|
329
517
|
<div className="shortcuts-panel">
|
|
@@ -346,6 +534,7 @@ export default function DashboardPage() {
|
|
|
346
534
|
<div className="shortcut-row"><kbd>G</kbd><span>Toggle guard</span></div>
|
|
347
535
|
<div className="shortcut-row"><kbd>D</kbd><span>Delete process</span></div>
|
|
348
536
|
<div className="shortcut-row"><kbd>N</kbd><span>New process</span></div>
|
|
537
|
+
<div className="shortcut-row"><kbd>T</kbd><span>Cycle stat filter</span></div>
|
|
349
538
|
<div className="shortcut-row"><kbd>?</kbd><span>This help</span></div>
|
|
350
539
|
</div>
|
|
351
540
|
</div>
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ __export(exports_platform, {
|
|
|
25
25
|
reconcileProcessPids: () => reconcileProcessPids,
|
|
26
26
|
readFileTail: () => readFileTail,
|
|
27
27
|
psExec: () => psExec,
|
|
28
|
+
parseUnixListeningPorts: () => parseUnixListeningPorts,
|
|
28
29
|
killProcessOnPort: () => killProcessOnPort,
|
|
29
30
|
isWindows: () => isWindows,
|
|
30
31
|
isProcessRunning: () => isProcessRunning,
|
|
@@ -80,7 +81,8 @@ async function isProcessRunning(pid, command) {
|
|
|
80
81
|
process.kill(pid, 0);
|
|
81
82
|
return true;
|
|
82
83
|
} catch {
|
|
83
|
-
|
|
84
|
+
const output = psExec(`Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id`).trim();
|
|
85
|
+
return output === String(pid);
|
|
84
86
|
}
|
|
85
87
|
} else {
|
|
86
88
|
const result = await $`ps -p ${pid}`.nothrow().text();
|
|
@@ -456,6 +458,17 @@ async function getProcessBatchResources(pids) {
|
|
|
456
458
|
return resourceMap;
|
|
457
459
|
}) ?? new Map;
|
|
458
460
|
}
|
|
461
|
+
function parseUnixListeningPorts(output) {
|
|
462
|
+
const ports = new Set;
|
|
463
|
+
for (const line of output.split(`
|
|
464
|
+
`)) {
|
|
465
|
+
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
466
|
+
if (portMatch) {
|
|
467
|
+
ports.add(parseInt(portMatch[1]));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return Array.from(ports);
|
|
471
|
+
}
|
|
459
472
|
async function getProcessPorts(pid) {
|
|
460
473
|
try {
|
|
461
474
|
if (isWindows()) {
|
|
@@ -472,31 +485,21 @@ async function getProcessPorts(pid) {
|
|
|
472
485
|
} else {
|
|
473
486
|
try {
|
|
474
487
|
const result2 = await $`ss -tlnp`.nothrow().quiet().text();
|
|
475
|
-
const
|
|
488
|
+
const ports = new Set;
|
|
476
489
|
for (const line of result2.split(`
|
|
477
490
|
`)) {
|
|
478
491
|
if (line.includes(`pid=${pid}`)) {
|
|
479
492
|
const portMatch = line.match(/:(\d+)\s/);
|
|
480
493
|
if (portMatch) {
|
|
481
|
-
|
|
494
|
+
ports.add(parseInt(portMatch[1]));
|
|
482
495
|
}
|
|
483
496
|
}
|
|
484
497
|
}
|
|
485
|
-
if (
|
|
486
|
-
return Array.from(
|
|
498
|
+
if (ports.size > 0)
|
|
499
|
+
return Array.from(ports);
|
|
487
500
|
} catch {}
|
|
488
|
-
const result = await $`lsof -
|
|
489
|
-
|
|
490
|
-
for (const line of result.split(`
|
|
491
|
-
`)) {
|
|
492
|
-
if (line.includes("LISTEN")) {
|
|
493
|
-
const portMatch = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
494
|
-
if (portMatch) {
|
|
495
|
-
ports.add(parseInt(portMatch[1]));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return Array.from(ports);
|
|
501
|
+
const result = await $`lsof -Pan -p ${pid} -iTCP -sTCP:LISTEN`.nothrow().quiet().text();
|
|
502
|
+
return parseUnixListeningPorts(result);
|
|
500
503
|
}
|
|
501
504
|
} catch {
|
|
502
505
|
return [];
|
|
@@ -595,12 +598,18 @@ __export(exports_db, {
|
|
|
595
598
|
retryDatabaseOperation: () => retryDatabaseOperation,
|
|
596
599
|
removeProcessByName: () => removeProcessByName,
|
|
597
600
|
removeProcess: () => removeProcess,
|
|
601
|
+
removeDependency: () => removeDependency,
|
|
598
602
|
removeAllProcesses: () => removeAllProcesses,
|
|
603
|
+
removeAllDependencies: () => removeAllDependencies,
|
|
599
604
|
insertProcess: () => insertProcess,
|
|
600
605
|
getTemplate: () => getTemplate,
|
|
606
|
+
getStartOrder: () => getStartOrder,
|
|
601
607
|
getRecentHistory: () => getRecentHistory,
|
|
602
608
|
getProcessHistory: () => getProcessHistory,
|
|
603
609
|
getProcess: () => getProcess,
|
|
610
|
+
getDependents: () => getDependents,
|
|
611
|
+
getDependencyGraph: () => getDependencyGraph,
|
|
612
|
+
getDependencies: () => getDependencies,
|
|
604
613
|
getDbInfo: () => getDbInfo,
|
|
605
614
|
getAllTemplates: () => getAllTemplates,
|
|
606
615
|
getAllProcesses: () => getAllProcesses,
|
|
@@ -610,9 +619,11 @@ __export(exports_db, {
|
|
|
610
619
|
clearOldHistory: () => clearOldHistory,
|
|
611
620
|
bgrHome: () => bgrHome,
|
|
612
621
|
addHistoryEntry: () => addHistoryEntry,
|
|
622
|
+
addDependency: () => addDependency,
|
|
613
623
|
TemplateSchema: () => TemplateSchema,
|
|
614
624
|
ProcessSchema: () => ProcessSchema,
|
|
615
|
-
HistorySchema: () => HistorySchema
|
|
625
|
+
HistorySchema: () => HistorySchema,
|
|
626
|
+
DependencySchema: () => DependencySchema
|
|
616
627
|
});
|
|
617
628
|
import { Database, z } from "sqlite-zod-orm";
|
|
618
629
|
import { join as join2 } from "path";
|
|
@@ -715,6 +726,99 @@ function clearOldHistory(daysToKeep = 30) {
|
|
|
715
726
|
}
|
|
716
727
|
return oldEntries.length;
|
|
717
728
|
}
|
|
729
|
+
function getDependencies(processName) {
|
|
730
|
+
return db.dependency.select().where({ process_name: processName }).all().map((d) => d.depends_on);
|
|
731
|
+
}
|
|
732
|
+
function getDependents(processName) {
|
|
733
|
+
return db.dependency.select().where({ depends_on: processName }).all().map((d) => d.process_name);
|
|
734
|
+
}
|
|
735
|
+
function getDependencyGraph() {
|
|
736
|
+
const all = db.dependency.select().all();
|
|
737
|
+
const graph = {};
|
|
738
|
+
for (const dep of all) {
|
|
739
|
+
if (!graph[dep.process_name])
|
|
740
|
+
graph[dep.process_name] = [];
|
|
741
|
+
graph[dep.process_name].push(dep.depends_on);
|
|
742
|
+
}
|
|
743
|
+
return graph;
|
|
744
|
+
}
|
|
745
|
+
function addDependency(processName, dependsOn) {
|
|
746
|
+
if (processName === dependsOn)
|
|
747
|
+
return false;
|
|
748
|
+
const existing = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).limit(1).get();
|
|
749
|
+
if (existing)
|
|
750
|
+
return false;
|
|
751
|
+
if (wouldCreateCycle(processName, dependsOn))
|
|
752
|
+
return false;
|
|
753
|
+
db.dependency.insert({ process_name: processName, depends_on: dependsOn });
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
function removeDependency(processName, dependsOn) {
|
|
757
|
+
const matches = db.dependency.select().where({ process_name: processName, depends_on: dependsOn }).all();
|
|
758
|
+
for (const dep of matches) {
|
|
759
|
+
db.dependency.delete(dep.id);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function removeAllDependencies(processName) {
|
|
763
|
+
const matches = db.dependency.select().where({ process_name: processName }).all();
|
|
764
|
+
for (const dep of matches) {
|
|
765
|
+
db.dependency.delete(dep.id);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function wouldCreateCycle(processName, dependsOn) {
|
|
769
|
+
const graph = getDependencyGraph();
|
|
770
|
+
if (!graph[processName])
|
|
771
|
+
graph[processName] = [];
|
|
772
|
+
graph[processName].push(dependsOn);
|
|
773
|
+
const visited = new Set;
|
|
774
|
+
const stack = [dependsOn];
|
|
775
|
+
while (stack.length > 0) {
|
|
776
|
+
const current = stack.pop();
|
|
777
|
+
if (current === processName)
|
|
778
|
+
return true;
|
|
779
|
+
if (visited.has(current))
|
|
780
|
+
continue;
|
|
781
|
+
visited.add(current);
|
|
782
|
+
for (const dep of graph[current] || []) {
|
|
783
|
+
stack.push(dep);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
function getStartOrder() {
|
|
789
|
+
const graph = getDependencyGraph();
|
|
790
|
+
const allProcesses = getAllProcesses().map((p) => p.name);
|
|
791
|
+
const allNames = new Set(allProcesses);
|
|
792
|
+
const inDegree = {};
|
|
793
|
+
for (const name of allNames)
|
|
794
|
+
inDegree[name] = 0;
|
|
795
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
796
|
+
for (const dep of deps) {
|
|
797
|
+
if (allNames.has(dep)) {
|
|
798
|
+
inDegree[proc] = (inDegree[proc] || 0) + 1;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const queue = [];
|
|
803
|
+
for (const name of allNames) {
|
|
804
|
+
if ((inDegree[name] || 0) === 0)
|
|
805
|
+
queue.push(name);
|
|
806
|
+
}
|
|
807
|
+
const order = [];
|
|
808
|
+
while (queue.length > 0) {
|
|
809
|
+
queue.sort();
|
|
810
|
+
const current = queue.shift();
|
|
811
|
+
order.push(current);
|
|
812
|
+
for (const [proc, deps] of Object.entries(graph)) {
|
|
813
|
+
if (deps.includes(current) && allNames.has(proc)) {
|
|
814
|
+
inDegree[proc]--;
|
|
815
|
+
if (inDegree[proc] === 0)
|
|
816
|
+
queue.push(proc);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return order;
|
|
821
|
+
}
|
|
718
822
|
function getDbInfo() {
|
|
719
823
|
return {
|
|
720
824
|
dbPath,
|
|
@@ -737,7 +841,7 @@ async function retryDatabaseOperation(operation, maxRetries = 5, delay = 100) {
|
|
|
737
841
|
}
|
|
738
842
|
throw new Error("Max retries reached for database operation");
|
|
739
843
|
}
|
|
740
|
-
var ProcessSchema, TemplateSchema, HistorySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
844
|
+
var ProcessSchema, TemplateSchema, HistorySchema, DependencySchema, homePath, bgrDir, dbFilename, dbPath, bgrHome, legacyDbPath, db;
|
|
741
845
|
var init_db = __esm(() => {
|
|
742
846
|
init_platform();
|
|
743
847
|
ProcessSchema = z.object({
|
|
@@ -767,6 +871,11 @@ var init_db = __esm(() => {
|
|
|
767
871
|
timestamp: z.string().default(() => new Date().toISOString()),
|
|
768
872
|
metadata: z.string().default("")
|
|
769
873
|
});
|
|
874
|
+
DependencySchema = z.object({
|
|
875
|
+
process_name: z.string(),
|
|
876
|
+
depends_on: z.string(),
|
|
877
|
+
created_at: z.string().default(() => new Date().toISOString())
|
|
878
|
+
});
|
|
770
879
|
homePath = getHomeDir();
|
|
771
880
|
bgrDir = join2(homePath, ".bgr");
|
|
772
881
|
ensureDir(bgrDir);
|
|
@@ -783,12 +892,14 @@ var init_db = __esm(() => {
|
|
|
783
892
|
db = new Database(dbPath, {
|
|
784
893
|
process: ProcessSchema,
|
|
785
894
|
template: TemplateSchema,
|
|
786
|
-
history: HistorySchema
|
|
895
|
+
history: HistorySchema,
|
|
896
|
+
dependency: DependencySchema
|
|
787
897
|
}, {
|
|
788
898
|
indexes: {
|
|
789
899
|
process: ["name", "timestamp", "pid"],
|
|
790
900
|
template: ["name"],
|
|
791
|
-
history: ["process_name", "timestamp"]
|
|
901
|
+
history: ["process_name", "timestamp"],
|
|
902
|
+
dependency: ["process_name", "depends_on"]
|
|
792
903
|
}
|
|
793
904
|
});
|
|
794
905
|
});
|
|
@@ -863,10 +974,10 @@ async function parseConfigFile(configPath) {
|
|
|
863
974
|
var exports_deps = {};
|
|
864
975
|
__export(exports_deps, {
|
|
865
976
|
getUnmetDeps: () => getUnmetDeps,
|
|
866
|
-
getDependencies: () =>
|
|
977
|
+
getDependencies: () => getDependencies2,
|
|
867
978
|
buildDepGraph: () => buildDepGraph
|
|
868
979
|
});
|
|
869
|
-
function
|
|
980
|
+
function getDependencies2(envStr) {
|
|
870
981
|
const env = parseEnvString(envStr);
|
|
871
982
|
const raw = env.BGR_DEPENDS_ON || "";
|
|
872
983
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -875,7 +986,7 @@ async function buildDepGraph() {
|
|
|
875
986
|
const processes = getAllProcesses();
|
|
876
987
|
const nodeMap = new Map;
|
|
877
988
|
for (const proc of processes) {
|
|
878
|
-
const deps =
|
|
989
|
+
const deps = getDependencies2(proc.env);
|
|
879
990
|
const alive = await isProcessRunning(proc.pid, proc.command);
|
|
880
991
|
nodeMap.set(proc.name, {
|
|
881
992
|
name: proc.name,
|
|
@@ -927,7 +1038,7 @@ async function getUnmetDeps(name) {
|
|
|
927
1038
|
const proc = getProcess(name);
|
|
928
1039
|
if (!proc)
|
|
929
1040
|
return [];
|
|
930
|
-
const deps =
|
|
1041
|
+
const deps = getDependencies2(proc.env);
|
|
931
1042
|
const unmet = [];
|
|
932
1043
|
for (const depName of deps) {
|
|
933
1044
|
const depProc = getProcess(depName);
|
|
@@ -2677,6 +2788,13 @@ async function run2() {
|
|
|
2677
2788
|
});
|
|
2678
2789
|
return;
|
|
2679
2790
|
}
|
|
2791
|
+
if (name === "list") {
|
|
2792
|
+
await showAll({
|
|
2793
|
+
json: values.json,
|
|
2794
|
+
filter: values.filter
|
|
2795
|
+
});
|
|
2796
|
+
return;
|
|
2797
|
+
}
|
|
2680
2798
|
if (name) {
|
|
2681
2799
|
if (!values.command && !values.directory) {
|
|
2682
2800
|
await showDetails(name);
|