bgrun 3.12.3 → 3.12.5
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/history/route.ts +73 -7
- package/dashboard/app/api/logs/[name]/route.ts +100 -67
- package/dashboard/app/globals.css +176 -168
- package/dashboard/app/page.client.tsx +36 -0
- package/dashboard/app/page.tsx +9 -0
- package/package.json +19 -4
- package/src/bgrun.test.ts +0 -313
- package/src/index_copy.ts +0 -614
|
@@ -1292,6 +1292,23 @@ export default function mount(): () => void {
|
|
|
1292
1292
|
else renderEnvPanel();
|
|
1293
1293
|
}
|
|
1294
1294
|
|
|
1295
|
+
function updateLogExportLinks() {
|
|
1296
|
+
const textBtn = $('log-export-text-btn') as HTMLAnchorElement | null;
|
|
1297
|
+
const jsonBtn = $('log-export-json-btn') as HTMLAnchorElement | null;
|
|
1298
|
+
const csvBtn = $('log-export-csv-btn') as HTMLAnchorElement | null;
|
|
1299
|
+
const disabledHref = '#';
|
|
1300
|
+
if (!drawerProcess) {
|
|
1301
|
+
if (textBtn) textBtn.href = disabledHref;
|
|
1302
|
+
if (jsonBtn) jsonBtn.href = disabledHref;
|
|
1303
|
+
if (csvBtn) csvBtn.href = disabledHref;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const base = `/api/logs/${encodeURIComponent(drawerProcess)}?tab=${drawerTab}&offset=0&download=1`;
|
|
1307
|
+
if (textBtn) textBtn.href = `${base}&format=text`;
|
|
1308
|
+
if (jsonBtn) jsonBtn.href = `${base}&format=json`;
|
|
1309
|
+
if (csvBtn) csvBtn.href = `${base}&format=csv`;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1295
1312
|
function switchLogSubtab(subtab: string, skipRefresh = false) {
|
|
1296
1313
|
drawerTab = subtab as 'stdout' | 'stderr';
|
|
1297
1314
|
$('log-subtabs')?.querySelectorAll('.accordion-subtab').forEach(btn => {
|
|
@@ -1305,6 +1322,7 @@ export default function mount(): () => void {
|
|
|
1305
1322
|
logNeedsFullRebuild = true;
|
|
1306
1323
|
logVirtualActive = false;
|
|
1307
1324
|
logFilteredIndices = [];
|
|
1325
|
+
updateLogExportLinks();
|
|
1308
1326
|
if (!skipRefresh) refreshDrawerLogs();
|
|
1309
1327
|
}
|
|
1310
1328
|
|
|
@@ -1354,6 +1372,7 @@ export default function mount(): () => void {
|
|
|
1354
1372
|
function openDrawer(name: string) {
|
|
1355
1373
|
drawerProcess = name;
|
|
1356
1374
|
drawerTab = 'stdout';
|
|
1375
|
+
updateLogExportLinks();
|
|
1357
1376
|
|
|
1358
1377
|
// Update header
|
|
1359
1378
|
const nameEl = $('drawer-process-name');
|
|
@@ -1501,6 +1520,7 @@ export default function mount(): () => void {
|
|
|
1501
1520
|
drawer?.classList.remove('open');
|
|
1502
1521
|
backdrop?.classList.remove('active');
|
|
1503
1522
|
drawerProcess = null;
|
|
1523
|
+
updateLogExportLinks();
|
|
1504
1524
|
tbody?.querySelectorAll('tr.selected').forEach(r => r.classList.remove('selected'));
|
|
1505
1525
|
}
|
|
1506
1526
|
|
|
@@ -2559,6 +2579,21 @@ export default function mount(): () => void {
|
|
|
2559
2579
|
}
|
|
2560
2580
|
}
|
|
2561
2581
|
|
|
2582
|
+
function updateHistoryExportLinks() {
|
|
2583
|
+
const processFilter = $('history-process-filter') as HTMLSelectElement | null;
|
|
2584
|
+
const eventFilter = $('history-event-filter') as HTMLSelectElement | null;
|
|
2585
|
+
const metadataFilter = $('history-metadata-filter') as HTMLInputElement | null;
|
|
2586
|
+
const params = new URLSearchParams({ limit: '100', download: '1' });
|
|
2587
|
+
if (processFilter?.value) params.set('name', processFilter.value);
|
|
2588
|
+
if (eventFilter?.value) params.set('event', eventFilter.value);
|
|
2589
|
+
if (metadataFilter?.value.trim()) params.set('metadata', metadataFilter.value.trim());
|
|
2590
|
+
|
|
2591
|
+
const jsonBtn = $('history-export-json-btn') as HTMLAnchorElement | null;
|
|
2592
|
+
const csvBtn = $('history-export-csv-btn') as HTMLAnchorElement | null;
|
|
2593
|
+
if (jsonBtn) jsonBtn.href = `/api/history?${params.toString()}&format=json`;
|
|
2594
|
+
if (csvBtn) csvBtn.href = `/api/history?${params.toString()}&format=csv`;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2562
2597
|
function updateHistoryFilters() {
|
|
2563
2598
|
const processFilter = $('history-process-filter') as HTMLSelectElement;
|
|
2564
2599
|
const eventFilter = $('history-event-filter') as HTMLSelectElement;
|
|
@@ -2827,6 +2862,7 @@ export default function mount(): () => void {
|
|
|
2827
2862
|
}
|
|
2828
2863
|
|
|
2829
2864
|
updateHistoryClearButton();
|
|
2865
|
+
updateHistoryExportLinks();
|
|
2830
2866
|
applyHistoryDensity();
|
|
2831
2867
|
applyHistoryShortcutPreference();
|
|
2832
2868
|
applyHistoryDetailsPreference();
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -266,6 +266,11 @@ export default function DashboardPage() {
|
|
|
266
266
|
<div className="drawer-log-toolbar" id="drawer-log-toolbar">
|
|
267
267
|
<input type="text" id="log-search" className="log-search" placeholder="Filter logs..." />
|
|
268
268
|
<span className="log-line-count" id="log-line-count"></span>
|
|
269
|
+
<div className="toolbar-export-actions">
|
|
270
|
+
<a id="log-export-text-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export .log</a>
|
|
271
|
+
<a id="log-export-json-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export JSON</a>
|
|
272
|
+
<a id="log-export-csv-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export CSV</a>
|
|
273
|
+
</div>
|
|
269
274
|
<button id="log-autoscroll-btn" className="log-autoscroll" title="Auto-scroll: OFF">
|
|
270
275
|
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7" /></svg>
|
|
271
276
|
Follow
|
|
@@ -387,6 +392,10 @@ export default function DashboardPage() {
|
|
|
387
392
|
</select>
|
|
388
393
|
</label>
|
|
389
394
|
<button className="btn btn-ghost btn-sm" id="history-clear-filters-btn" title="Clear all history filters">Clear</button>
|
|
395
|
+
<div className="toolbar-export-actions">
|
|
396
|
+
<a id="history-export-json-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export JSON</a>
|
|
397
|
+
<a id="history-export-csv-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export CSV</a>
|
|
398
|
+
</div>
|
|
390
399
|
</div>
|
|
391
400
|
<div className="history-hints-bar">
|
|
392
401
|
<div className="history-hints-bar-left">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bgrun",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.5",
|
|
4
4
|
"description": "bgrun — A lightweight process manager for Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/api.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
".": "./src/api.ts"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
-
"bgrun": "
|
|
11
|
+
"bgrun": "dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "bun run ./src/build.ts",
|
|
@@ -17,7 +17,22 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
-
"src",
|
|
20
|
+
"src/api.ts",
|
|
21
|
+
"src/build.ts",
|
|
22
|
+
"src/config.ts",
|
|
23
|
+
"src/db.ts",
|
|
24
|
+
"src/deploy.ts",
|
|
25
|
+
"src/deps.ts",
|
|
26
|
+
"src/guard.ts",
|
|
27
|
+
"src/index.ts",
|
|
28
|
+
"src/log-rotation.ts",
|
|
29
|
+
"src/logger.ts",
|
|
30
|
+
"src/platform.ts",
|
|
31
|
+
"src/server.ts",
|
|
32
|
+
"src/table.ts",
|
|
33
|
+
"src/types.ts",
|
|
34
|
+
"src/utils.ts",
|
|
35
|
+
"src/commands",
|
|
21
36
|
"dashboard/app",
|
|
22
37
|
"scripts",
|
|
23
38
|
"README.md",
|
|
@@ -37,7 +52,7 @@
|
|
|
37
52
|
"license": "MIT",
|
|
38
53
|
"repository": {
|
|
39
54
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/7flash/bgrun.git"
|
|
55
|
+
"url": "git+https://github.com/7flash/bgrun.git"
|
|
41
56
|
},
|
|
42
57
|
"devDependencies": {
|
|
43
58
|
"@types/bun": "^1.3.10",
|
package/src/bgrun.test.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bgrun core utility tests
|
|
3
|
-
*
|
|
4
|
-
* Tests pure logic functions: env parsing, config flattening,
|
|
5
|
-
* string truncation, and runtime calculation.
|
|
6
|
-
*
|
|
7
|
-
* Run: bun test src/bgrun.test.ts
|
|
8
|
-
*/
|
|
9
|
-
import { describe, expect, test } from 'bun:test'
|
|
10
|
-
import { parseEnvString, calculateRuntime } from './utils'
|
|
11
|
-
import { stripAnsi, truncateString, truncatePath } from './table'
|
|
12
|
-
import { detectPackageManager, formatDeployToolError } from './deploy'
|
|
13
|
-
import { isProcessRunning, parseUnixListeningPorts } from './platform'
|
|
14
|
-
import { mkdirSync, rmSync } from 'fs'
|
|
15
|
-
|
|
16
|
-
// Use a test-specific database to avoid polluting real data
|
|
17
|
-
process.env.BGRUN_DB = `bgrun-test-${Date.now()}.sqlite`
|
|
18
|
-
import { addDependency, removeDependency, getDependencyGraph, getDependencies, getDependents, getStartOrder, removeAllDependencies } from './db'
|
|
19
|
-
|
|
20
|
-
// ─── parseEnvString ─────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
describe('parseEnvString', () => {
|
|
23
|
-
test('parses comma-separated key=value pairs', () => {
|
|
24
|
-
const result = parseEnvString('PORT=3000,HOST=localhost,DEBUG=true')
|
|
25
|
-
expect(result).toEqual({
|
|
26
|
-
PORT: '3000',
|
|
27
|
-
HOST: 'localhost',
|
|
28
|
-
DEBUG: 'true',
|
|
29
|
-
})
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('handles single pair', () => {
|
|
33
|
-
expect(parseEnvString('KEY=value')).toEqual({ KEY: 'value' })
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('handles empty string', () => {
|
|
37
|
-
expect(parseEnvString('')).toEqual({})
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('ignores malformed pairs (no =)', () => {
|
|
41
|
-
const result = parseEnvString('GOOD=yes,BAD,ALSO_GOOD=ok')
|
|
42
|
-
expect(result.GOOD).toBe('yes')
|
|
43
|
-
expect(result.ALSO_GOOD).toBe('ok')
|
|
44
|
-
expect(result.BAD).toBeUndefined()
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// ─── calculateRuntime ───────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe('calculateRuntime', () => {
|
|
51
|
-
test('returns 0 minutes for recent start', () => {
|
|
52
|
-
const now = new Date().toISOString()
|
|
53
|
-
expect(calculateRuntime(now)).toBe('0 minutes')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
test('returns correct minutes', () => {
|
|
57
|
-
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString()
|
|
58
|
-
expect(calculateRuntime(fiveMinAgo)).toBe('5 minutes')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('returns correct for 1 hour', () => {
|
|
62
|
-
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
|
|
63
|
-
expect(calculateRuntime(oneHourAgo)).toBe('60 minutes')
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
// ─── stripAnsi ──────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
describe('stripAnsi', () => {
|
|
70
|
-
test('strips color codes', () => {
|
|
71
|
-
const colored = '\u001b[31mred text\u001b[0m'
|
|
72
|
-
expect(stripAnsi(colored)).toBe('red text')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test('passes through plain text', () => {
|
|
76
|
-
expect(stripAnsi('hello world')).toBe('hello world')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('handles empty string', () => {
|
|
80
|
-
expect(stripAnsi('')).toBe('')
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// ─── truncateString ─────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
describe('truncateString', () => {
|
|
87
|
-
test('returns string unchanged if within limit', () => {
|
|
88
|
-
expect(truncateString('hello', 10)).toBe('hello')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('truncates with ellipsis', () => {
|
|
92
|
-
const result = truncateString('a very long string that exceeds limit', 15)
|
|
93
|
-
expect(result.length).toBeLessThanOrEqual(15)
|
|
94
|
-
expect(result).toContain('…')
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('handles maxLength smaller than ellipsis', () => {
|
|
98
|
-
const result = truncateString('hello world', 2)
|
|
99
|
-
expect(result.length).toBeLessThanOrEqual(2)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// ─── truncatePath ───────────────────────────────────────
|
|
104
|
-
|
|
105
|
-
describe('truncatePath', () => {
|
|
106
|
-
test('returns path unchanged if within limit', () => {
|
|
107
|
-
expect(truncatePath('/home/user', 50)).toBe('/home/user')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('truncates middle of long path', () => {
|
|
111
|
-
const longPath = '/home/user/projects/very/deeply/nested/directory/structure'
|
|
112
|
-
const result = truncatePath(longPath, 30)
|
|
113
|
-
expect(result.length).toBeLessThanOrEqual(30)
|
|
114
|
-
expect(result).toContain('…')
|
|
115
|
-
})
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
// ─── detectPackageManager ───────────────────────────────
|
|
119
|
-
|
|
120
|
-
// ─── isProcessRunning (Windows liveness fallback) ───────
|
|
121
|
-
|
|
122
|
-
describe('isProcessRunning', () => {
|
|
123
|
-
test('returns true for the current process PID', async () => {
|
|
124
|
-
const alive = await isProcessRunning(process.pid)
|
|
125
|
-
expect(alive).toBe(true)
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
test('returns false for PID 0 (intentionally stopped)', async () => {
|
|
129
|
-
const alive = await isProcessRunning(0)
|
|
130
|
-
expect(alive).toBe(false)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test('returns false for a very high unlikely PID', async () => {
|
|
134
|
-
const alive = await isProcessRunning(999999)
|
|
135
|
-
expect(alive).toBe(false)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('returns false for negative PID', async () => {
|
|
139
|
-
const alive = await isProcessRunning(-1)
|
|
140
|
-
expect(alive).toBe(false)
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe('parseUnixListeningPorts', () => {
|
|
145
|
-
test('extracts only LISTEN ports from lsof output', () => {
|
|
146
|
-
const output = [
|
|
147
|
-
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
148
|
-
'bun 12345 root 21u IPv4 123456 0t0 TCP *:3400 (LISTEN)',
|
|
149
|
-
'bun 12345 root 22u IPv4 123457 0t0 TCP 127.0.0.1:9222 (LISTEN)',
|
|
150
|
-
].join('\n')
|
|
151
|
-
|
|
152
|
-
expect(parseUnixListeningPorts(output)).toEqual([3400, 9222])
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
test('ignores non-LISTEN sockets from broad lsof output', () => {
|
|
156
|
-
const output = [
|
|
157
|
-
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
158
|
-
'bun 12345 root 18u IPv4 111111 0t0 TCP 127.0.0.1:49440->127.0.0.1:3000 (ESTABLISHED)',
|
|
159
|
-
'bun 12345 root 19u IPv4 111112 0t0 TCP 127.0.0.1:49441->127.0.0.1:3737 (ESTABLISHED)',
|
|
160
|
-
'bun 12345 root 20u IPv4 111113 0t0 TCP *:3400 (LISTEN)',
|
|
161
|
-
].join('\n')
|
|
162
|
-
|
|
163
|
-
expect(parseUnixListeningPorts(output)).toEqual([3400])
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('returns empty array for no-port worker output', () => {
|
|
167
|
-
const output = [
|
|
168
|
-
'COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME',
|
|
169
|
-
'bun 12345 root 18u unix 0xffff 0t0 /tmp/bun.sock',
|
|
170
|
-
'bun 12345 root 19u IPv4 111111 0t0 TCP 127.0.0.1:49440->127.0.0.1:3000 (ESTABLISHED)',
|
|
171
|
-
].join('\n')
|
|
172
|
-
|
|
173
|
-
expect(parseUnixListeningPorts(output)).toEqual([])
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
// ─── detectPackageManager ───────────────────────────────
|
|
178
|
-
|
|
179
|
-
describe('formatDeployToolError', () => {
|
|
180
|
-
test('returns actionable message for missing binary', () => {
|
|
181
|
-
const msg = formatDeployToolError('pnpm', new Error('command not found: pnpm'))
|
|
182
|
-
expect(msg).toContain("requires 'pnpm'")
|
|
183
|
-
expect(msg).toContain('PATH')
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test('preserves non-missing-binary failures', () => {
|
|
187
|
-
const msg = formatDeployToolError('npm', new Error('npm ci failed with exit code 1'))
|
|
188
|
-
expect(msg).toContain('Dependency install failed with npm')
|
|
189
|
-
expect(msg).toContain('exit code 1')
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
describe('detectPackageManager', () => {
|
|
194
|
-
test('returns null when no package.json exists', async () => {
|
|
195
|
-
const dir = `${process.cwd()}/tmp-no-package-${Date.now()}`
|
|
196
|
-
mkdirSync(dir, { recursive: true })
|
|
197
|
-
try {
|
|
198
|
-
expect(await detectPackageManager(dir)).toBeNull()
|
|
199
|
-
} finally {
|
|
200
|
-
rmSync(dir, { recursive: true, force: true })
|
|
201
|
-
}
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('prefers bun lockfiles', async () => {
|
|
205
|
-
const dir = `${process.cwd()}/tmp-bun-${Date.now()}`
|
|
206
|
-
mkdirSync(dir, { recursive: true })
|
|
207
|
-
try {
|
|
208
|
-
await Bun.write(`${dir}/package.json`, '{}')
|
|
209
|
-
await Bun.write(`${dir}/bun.lock`, '')
|
|
210
|
-
expect(await detectPackageManager(dir)).toBe('bun')
|
|
211
|
-
} finally {
|
|
212
|
-
rmSync(dir, { recursive: true, force: true })
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
test('detects pnpm, yarn, and npm lockfiles', async () => {
|
|
217
|
-
const base = `${process.cwd()}/tmp-pm-${Date.now()}`
|
|
218
|
-
|
|
219
|
-
const pnpmDir = `${base}-pnpm`
|
|
220
|
-
mkdirSync(pnpmDir, { recursive: true })
|
|
221
|
-
await Bun.write(`${pnpmDir}/package.json`, '{}')
|
|
222
|
-
await Bun.write(`${pnpmDir}/pnpm-lock.yaml`, '')
|
|
223
|
-
expect(await detectPackageManager(pnpmDir)).toBe('pnpm')
|
|
224
|
-
|
|
225
|
-
const yarnDir = `${base}-yarn`
|
|
226
|
-
mkdirSync(yarnDir, { recursive: true })
|
|
227
|
-
await Bun.write(`${yarnDir}/package.json`, '{}')
|
|
228
|
-
await Bun.write(`${yarnDir}/yarn.lock`, '')
|
|
229
|
-
expect(await detectPackageManager(yarnDir)).toBe('yarn')
|
|
230
|
-
|
|
231
|
-
const npmDir = `${base}-npm`
|
|
232
|
-
mkdirSync(npmDir, { recursive: true })
|
|
233
|
-
await Bun.write(`${npmDir}/package.json`, '{}')
|
|
234
|
-
await Bun.write(`${npmDir}/package-lock.json`, '{}')
|
|
235
|
-
expect(await detectPackageManager(npmDir)).toBe('npm')
|
|
236
|
-
|
|
237
|
-
rmSync(pnpmDir, { recursive: true, force: true })
|
|
238
|
-
rmSync(yarnDir, { recursive: true, force: true })
|
|
239
|
-
rmSync(npmDir, { recursive: true, force: true })
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
test('defaults to bun for package.json projects without a lockfile', async () => {
|
|
243
|
-
const dir = `${process.cwd()}/tmp-default-bun-${Date.now()}`
|
|
244
|
-
mkdirSync(dir, { recursive: true })
|
|
245
|
-
try {
|
|
246
|
-
await Bun.write(`${dir}/package.json`, '{}')
|
|
247
|
-
expect(await detectPackageManager(dir)).toBe('bun')
|
|
248
|
-
} finally {
|
|
249
|
-
rmSync(dir, { recursive: true, force: true })
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
// ─── Dependencies ───────────────────────────────────────
|
|
255
|
-
|
|
256
|
-
describe('addDependency', () => {
|
|
257
|
-
test('adds a valid dependency', () => {
|
|
258
|
-
removeAllDependencies('web-server');
|
|
259
|
-
removeAllDependencies('database');
|
|
260
|
-
const ok = addDependency('web-server', 'database');
|
|
261
|
-
expect(ok).toBe(true);
|
|
262
|
-
expect(getDependencies('web-server')).toContain('database');
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
test('prevents self-dependency', () => {
|
|
266
|
-
expect(addDependency('api', 'api')).toBe(false);
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
test('prevents duplicate dependency', () => {
|
|
270
|
-
removeAllDependencies('app');
|
|
271
|
-
addDependency('app', 'db');
|
|
272
|
-
expect(addDependency('app', 'db')).toBe(false);
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
test('prevents circular dependency', () => {
|
|
276
|
-
removeAllDependencies('a');
|
|
277
|
-
removeAllDependencies('b');
|
|
278
|
-
removeAllDependencies('c');
|
|
279
|
-
addDependency('a', 'b');
|
|
280
|
-
addDependency('b', 'c');
|
|
281
|
-
// c -> a would create a cycle
|
|
282
|
-
expect(addDependency('c', 'a')).toBe(false);
|
|
283
|
-
})
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
describe('getDependencyGraph', () => {
|
|
287
|
-
test('returns full graph', () => {
|
|
288
|
-
removeAllDependencies('svc-a');
|
|
289
|
-
removeAllDependencies('svc-b');
|
|
290
|
-
addDependency('svc-a', 'svc-b');
|
|
291
|
-
const graph = getDependencyGraph();
|
|
292
|
-
expect(graph['svc-a']).toContain('svc-b');
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
describe('getDependents', () => {
|
|
297
|
-
test('finds processes that depend on a target', () => {
|
|
298
|
-
removeAllDependencies('frontend');
|
|
299
|
-
removeAllDependencies('backend');
|
|
300
|
-
addDependency('frontend', 'backend');
|
|
301
|
-
expect(getDependents('backend')).toContain('frontend');
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('removeDependency', () => {
|
|
306
|
-
test('removes an existing dependency', () => {
|
|
307
|
-
removeAllDependencies('x');
|
|
308
|
-
addDependency('x', 'y');
|
|
309
|
-
expect(getDependencies('x')).toContain('y');
|
|
310
|
-
removeDependency('x', 'y');
|
|
311
|
-
expect(getDependencies('x')).not.toContain('y');
|
|
312
|
-
})
|
|
313
|
-
})
|