bgrun 3.4.0 → 3.7.0
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/config/[name]/route.ts +55 -0
- package/dashboard/app/api/debug/route.ts +3 -2
- package/dashboard/app/api/events/route.ts +60 -0
- package/dashboard/app/api/logs/[name]/route.ts +55 -5
- package/dashboard/app/api/processes/[name]/route.ts +5 -2
- package/dashboard/app/api/processes/route.ts +88 -44
- package/dashboard/app/api/restart/[name]/route.ts +4 -3
- package/dashboard/app/api/start/route.ts +4 -3
- package/dashboard/app/api/stop/[name]/route.ts +11 -4
- package/dashboard/app/api/version/route.ts +4 -2
- package/dashboard/app/globals.css +652 -79
- package/dashboard/app/layout.tsx +2 -23
- package/dashboard/app/page.client.tsx +683 -107
- package/dashboard/app/page.tsx +97 -33
- package/dist/index.js +170 -128
- package/package.json +5 -2
- package/src/commands/list.ts +1 -0
- package/src/commands/run.ts +60 -47
- package/src/db.ts +27 -5
- package/src/index.ts +8 -1
- package/src/platform.ts +202 -96
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/config/:name — Read .config.toml content
|
|
3
|
+
* PUT /api/config/:name — Write .config.toml content
|
|
4
|
+
*/
|
|
5
|
+
import { getProcess } from '../../../../../src/db';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
8
|
+
|
|
9
|
+
function resolveConfigPath(proc: any): string | null {
|
|
10
|
+
if (!proc.configPath) return null;
|
|
11
|
+
return join(proc.workdir, proc.configPath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request, { params }: { params: { name: string } }) {
|
|
15
|
+
const name = decodeURIComponent(params.name);
|
|
16
|
+
const proc = getProcess(name);
|
|
17
|
+
|
|
18
|
+
if (!proc) {
|
|
19
|
+
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const configPath = resolveConfigPath(proc);
|
|
23
|
+
if (!configPath) {
|
|
24
|
+
return Response.json({ content: '', path: null, exists: false });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const content = await readFile(configPath, 'utf-8');
|
|
29
|
+
return Response.json({ content, path: configPath, exists: true });
|
|
30
|
+
} catch {
|
|
31
|
+
return Response.json({ content: '', path: configPath, exists: false });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function PUT(req: Request, { params }: { params: { name: string } }) {
|
|
36
|
+
const name = decodeURIComponent(params.name);
|
|
37
|
+
const proc = getProcess(name);
|
|
38
|
+
|
|
39
|
+
if (!proc) {
|
|
40
|
+
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const configPath = resolveConfigPath(proc);
|
|
44
|
+
if (!configPath) {
|
|
45
|
+
return Response.json({ error: 'No config path configured' }, { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const body = await req.json();
|
|
50
|
+
await writeFile(configPath, body.content, 'utf-8');
|
|
51
|
+
return Response.json({ success: true, path: configPath });
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
return Response.json({ error: e.message }, { status: 500 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Returns DB path, BGR home dir, platform info for diagnostics.
|
|
5
5
|
*/
|
|
6
|
-
import { getDbInfo } from '
|
|
6
|
+
import { getDbInfo } from '../../../../src/db';
|
|
7
|
+
import { measureSync } from 'measure-fn';
|
|
7
8
|
|
|
8
9
|
export async function GET() {
|
|
9
|
-
const info = getDbInfo();
|
|
10
|
+
const info = measureSync('DB info', () => getDbInfo());
|
|
10
11
|
return Response.json({
|
|
11
12
|
...info,
|
|
12
13
|
platform: process.platform,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/events — Server-Sent Events endpoint
|
|
3
|
+
*
|
|
4
|
+
* Streams process data every 3 seconds. Replaces client-side polling.
|
|
5
|
+
* The client connects once via EventSource and receives updates automatically.
|
|
6
|
+
*
|
|
7
|
+
* Delegates to /api/processes for data enrichment (including PID reconciliation).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const INTERVAL_MS = 3_000;
|
|
11
|
+
|
|
12
|
+
export async function GET(req: Request) {
|
|
13
|
+
const url = new URL(req.url);
|
|
14
|
+
const origin = url.origin; // e.g. http://localhost:3001
|
|
15
|
+
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
|
|
18
|
+
const stream = new ReadableStream({
|
|
19
|
+
async start(controller) {
|
|
20
|
+
// Send initial data immediately
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${origin}/api/processes?t=${Date.now()}`);
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
25
|
+
} catch {
|
|
26
|
+
controller.enqueue(encoder.encode(`data: []\n\n`));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
controller.enqueue(encoder.encode(`: keepalive\n\n`));
|
|
30
|
+
|
|
31
|
+
// Then send updates every INTERVAL_MS
|
|
32
|
+
const interval = setInterval(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${origin}/api/processes?t=${Date.now()}`);
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
|
|
37
|
+
} catch {
|
|
38
|
+
// Skip this tick, send on next
|
|
39
|
+
}
|
|
40
|
+
}, INTERVAL_MS);
|
|
41
|
+
|
|
42
|
+
// Store cleanup for when the stream is cancelled
|
|
43
|
+
(stream as any).__cleanup = () => clearInterval(interval);
|
|
44
|
+
},
|
|
45
|
+
cancel() {
|
|
46
|
+
if ((stream as any).__cleanup) {
|
|
47
|
+
(stream as any).__cleanup();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return new Response(stream, {
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'text/event-stream',
|
|
55
|
+
'Cache-Control': 'no-cache',
|
|
56
|
+
'Connection': 'keep-alive',
|
|
57
|
+
'X-Accel-Buffering': 'no',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/logs/:name — Read
|
|
2
|
+
* GET /api/logs/:name — Read process stdout/stderr logs
|
|
3
|
+
*
|
|
4
|
+
* Supports incremental loading via query params:
|
|
5
|
+
* ?tab=stdout|stderr — which log to read (default: stdout)
|
|
6
|
+
* ?offset=N — byte offset to start reading from (default: 0 = full file)
|
|
7
|
+
*
|
|
8
|
+
* Returns:
|
|
9
|
+
* { text, size, mtime, filePath }
|
|
10
|
+
*
|
|
11
|
+
* On first call (offset=0), returns full file content.
|
|
12
|
+
* On subsequent calls (offset=previousSize), returns only new bytes.
|
|
13
|
+
* Client uses `size` as the offset for the next request.
|
|
3
14
|
*/
|
|
4
|
-
import { getProcess
|
|
15
|
+
import { getProcess } from '../../../../../src/db';
|
|
16
|
+
import { stat, open } from 'fs/promises';
|
|
17
|
+
|
|
18
|
+
interface FileInfo {
|
|
19
|
+
text: string;
|
|
20
|
+
size: number;
|
|
21
|
+
mtime: string | null;
|
|
22
|
+
filePath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readLogFile(path: string, offset: number): Promise<FileInfo> {
|
|
26
|
+
try {
|
|
27
|
+
const s = await stat(path);
|
|
28
|
+
const size = s.size;
|
|
29
|
+
const mtime = s.mtime.toISOString();
|
|
30
|
+
|
|
31
|
+
// If offset >= current size, no new data
|
|
32
|
+
if (offset >= size) {
|
|
33
|
+
return { text: '', size, mtime, filePath: path };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Read from offset to end
|
|
37
|
+
const handle = await open(path, 'r');
|
|
38
|
+
try {
|
|
39
|
+
const bytesToRead = size - offset;
|
|
40
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
41
|
+
await handle.read(buffer, 0, bytesToRead, offset);
|
|
42
|
+
return { text: buffer.toString('utf-8'), size, mtime, filePath: path };
|
|
43
|
+
} finally {
|
|
44
|
+
await handle.close();
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
return { text: '', size: 0, mtime: null, filePath: path };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
5
50
|
|
|
6
51
|
export async function GET(req: Request, { params }: { params: { name: string } }) {
|
|
7
52
|
const name = decodeURIComponent(params.name);
|
|
@@ -11,7 +56,12 @@ export async function GET(req: Request, { params }: { params: { name: string } }
|
|
|
11
56
|
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
12
57
|
}
|
|
13
58
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
const tab = url.searchParams.get('tab') || 'stdout';
|
|
61
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10) || 0;
|
|
62
|
+
|
|
63
|
+
const path = tab === 'stderr' ? proc.stderr_path : proc.stdout_path;
|
|
64
|
+
const info = await readLogFile(path, offset);
|
|
65
|
+
|
|
66
|
+
return Response.json(info);
|
|
17
67
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DELETE /api/processes/:name — Stop and remove a process
|
|
3
3
|
*/
|
|
4
|
-
import { getProcess, removeProcessByName
|
|
4
|
+
import { getProcess, removeProcessByName } from '../../../../../src/db';
|
|
5
|
+
import { isProcessRunning, terminateProcess } from '../../../../../src/platform';
|
|
6
|
+
import { measure } from 'measure-fn';
|
|
5
7
|
|
|
6
8
|
export async function DELETE(req: Request, { params }: { params: { name: string } }) {
|
|
7
9
|
const name = decodeURIComponent(params.name);
|
|
@@ -12,8 +14,9 @@ export async function DELETE(req: Request, { params }: { params: { name: string
|
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
if (await isProcessRunning(proc.pid)) {
|
|
15
|
-
await terminateProcess(proc.pid);
|
|
17
|
+
await measure(`Terminate "${name}" before delete`, () => terminateProcess(proc.pid));
|
|
16
18
|
}
|
|
19
|
+
|
|
17
20
|
removeProcessByName(name);
|
|
18
21
|
return Response.json({ success: true });
|
|
19
22
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* instead of per-process calls to avoid subprocess pile-up on Windows.
|
|
6
|
-
* Results are cached for 5 seconds via globalThis.
|
|
7
|
-
*/
|
|
8
|
-
import { getAllProcesses, calculateRuntime, getProcessBatchMemory, getDbInfo } from 'bgrun';
|
|
1
|
+
import { getAllProcesses, updateProcessPid } from '../../../../src/db';
|
|
2
|
+
import { calculateRuntime } from '../../../../src/utils';
|
|
3
|
+
import { getProcessBatchMemory, reconcileProcessPids } from '../../../../src/platform';
|
|
4
|
+
import { measure, createMeasure } from 'measure-fn';
|
|
9
5
|
import { $ } from 'bun';
|
|
10
6
|
|
|
7
|
+
const api = createMeasure('api');
|
|
8
|
+
|
|
11
9
|
const CACHE_TTL_MS = 5_000;
|
|
12
10
|
const SUBPROCESS_TIMEOUT_MS = 4_000;
|
|
13
11
|
|
|
@@ -105,46 +103,87 @@ function getProcessGroup(envStr: string): string | null {
|
|
|
105
103
|
return match ? match[1] : null;
|
|
106
104
|
}
|
|
107
105
|
|
|
108
|
-
// ... existing code ...
|
|
109
|
-
|
|
110
106
|
async function fetchProcesses(): Promise<any[]> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
107
|
+
return await api.measure('Fetch processes', async (m) => {
|
|
108
|
+
const procs = getAllProcesses();
|
|
109
|
+
const pids = procs.map((p: any) => p.pid);
|
|
110
|
+
|
|
111
|
+
// Three subprocess calls total (not 3×N)
|
|
112
|
+
let [runningPids, portMap, memoryMap] = await Promise.all([
|
|
113
|
+
m('Running PIDs', () => withTimeout(getRunningPids(pids), new Set<number>())),
|
|
114
|
+
m('Port map', () => withTimeout(getPortsByPid(pids), new Map<number, number[]>())),
|
|
115
|
+
m('Memory map', () => withTimeout(getProcessBatchMemory(pids), new Map<number, number>())),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
// PID reconciliation: if stored PIDs are dead, try to find the real process
|
|
119
|
+
const allPids = new Set(pids);
|
|
120
|
+
const deadPids = new Set(pids.filter((pid: number) => !runningPids?.has(pid)));
|
|
121
|
+
|
|
122
|
+
if (deadPids.size > 0) {
|
|
123
|
+
const reconciled = await m('Reconcile dead PIDs', () =>
|
|
124
|
+
withTimeout(reconcileProcessPids(procs, deadPids), new Map<string, number>())
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (reconciled && reconciled.size > 0) {
|
|
128
|
+
// Update stored PIDs in DB and refresh running status
|
|
129
|
+
const newPids: number[] = [];
|
|
130
|
+
for (const [name, newPid] of reconciled) {
|
|
131
|
+
updateProcessPid(name, newPid);
|
|
132
|
+
newPids.push(newPid);
|
|
133
|
+
// Update the proc object in-place so the response uses the new PID
|
|
134
|
+
const proc = procs.find((p: any) => p.name === name);
|
|
135
|
+
if (proc) proc.pid = newPid;
|
|
136
|
+
}
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
// Mark reconciled PIDs as running
|
|
139
|
+
if (!runningPids) runningPids = new Set();
|
|
140
|
+
for (const pid of newPids) runningPids.add(pid);
|
|
141
|
+
|
|
142
|
+
// Re-fetch ports and memory for the new PIDs
|
|
143
|
+
const [newPorts, newMem] = await Promise.all([
|
|
144
|
+
withTimeout(getPortsByPid(newPids), new Map<number, number[]>()),
|
|
145
|
+
withTimeout(getProcessBatchMemory(newPids), new Map<number, number>()),
|
|
146
|
+
]);
|
|
147
|
+
if (!portMap) portMap = new Map();
|
|
148
|
+
if (!memoryMap) memoryMap = new Map();
|
|
149
|
+
for (const [pid, ports] of newPorts) portMap.set(pid, ports);
|
|
150
|
+
for (const [pid, mem] of newMem) memoryMap.set(pid, mem);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return procs.map((p: any) => {
|
|
155
|
+
const running = runningPids?.has(p.pid) ?? false;
|
|
156
|
+
const ports = running ? (portMap?.get(p.pid) || []) : [];
|
|
157
|
+
const memory = running ? (memoryMap?.get(p.pid) || 0) : 0;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: p.name,
|
|
161
|
+
command: p.command,
|
|
162
|
+
directory: p.workdir,
|
|
163
|
+
pid: p.pid,
|
|
164
|
+
running,
|
|
165
|
+
port: ports.length > 0 ? ports[0] : null,
|
|
166
|
+
ports,
|
|
167
|
+
memory, // Bytes
|
|
168
|
+
group: getProcessGroup(p.env),
|
|
169
|
+
runtime: calculateRuntime(p.timestamp),
|
|
170
|
+
timestamp: p.timestamp,
|
|
171
|
+
env: p.env || '',
|
|
172
|
+
configPath: p.configPath || '',
|
|
173
|
+
stdoutPath: p.stdout_path || '',
|
|
174
|
+
stderrPath: p.stderr_path || '',
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}) ?? [];
|
|
141
178
|
}
|
|
142
179
|
|
|
143
|
-
export async function GET() {
|
|
180
|
+
export async function GET(req: Request) {
|
|
181
|
+
const url = new URL(req.url);
|
|
182
|
+
const bustCache = url.searchParams.has('t');
|
|
144
183
|
const now = Date.now();
|
|
145
184
|
|
|
146
|
-
// Return cached data if still fresh
|
|
147
|
-
if (cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
|
|
185
|
+
// Return cached data if still fresh and no bust param
|
|
186
|
+
if (!bustCache && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
|
|
148
187
|
return Response.json(cache.data);
|
|
149
188
|
}
|
|
150
189
|
|
|
@@ -161,6 +200,11 @@ export async function GET() {
|
|
|
161
200
|
});
|
|
162
201
|
}
|
|
163
202
|
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
try {
|
|
204
|
+
const result = await cache.inflight;
|
|
205
|
+
return Response.json(result);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error('[api/processes] Error fetching processes:', err);
|
|
208
|
+
return Response.json(cache.data ?? []);
|
|
209
|
+
}
|
|
166
210
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /api/restart/:name — Force-restart a process
|
|
3
3
|
*/
|
|
4
|
-
import { handleRun } from '
|
|
4
|
+
import { handleRun } from '../../../../../src/commands/run';
|
|
5
|
+
import { measure } from 'measure-fn';
|
|
5
6
|
|
|
6
7
|
export async function POST(req: Request, { params }: { params: { name: string } }) {
|
|
7
8
|
const name = decodeURIComponent(params.name);
|
|
8
9
|
|
|
9
10
|
try {
|
|
10
|
-
await handleRun({
|
|
11
|
+
await measure(`Restart "${name}"`, () => handleRun({
|
|
11
12
|
action: 'run',
|
|
12
13
|
name,
|
|
13
14
|
force: true,
|
|
14
15
|
remoteName: '',
|
|
15
|
-
});
|
|
16
|
+
}));
|
|
16
17
|
return Response.json({ success: true });
|
|
17
18
|
} catch (e: any) {
|
|
18
19
|
return Response.json({ error: e.message }, { status: 500 });
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /api/start — Create or start a process
|
|
3
3
|
*/
|
|
4
|
-
import { handleRun } from '
|
|
4
|
+
import { handleRun } from '../../../../src/commands/run';
|
|
5
|
+
import { measure } from 'measure-fn';
|
|
5
6
|
|
|
6
7
|
export async function POST(req: Request) {
|
|
7
8
|
const body = await req.json();
|
|
8
9
|
|
|
9
10
|
try {
|
|
10
|
-
await handleRun({
|
|
11
|
+
await measure(`Start process "${body.name}"`, () => handleRun({
|
|
11
12
|
action: 'run',
|
|
12
13
|
name: body.name,
|
|
13
14
|
command: body.command,
|
|
14
15
|
directory: body.directory,
|
|
15
16
|
force: body.force || false,
|
|
16
17
|
remoteName: '',
|
|
17
|
-
});
|
|
18
|
+
}));
|
|
18
19
|
return Response.json({ success: true });
|
|
19
20
|
} catch (e: any) {
|
|
20
21
|
return Response.json({ error: e.message }, { status: 500 });
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /api/stop/:name — Stop a running process
|
|
3
3
|
*/
|
|
4
|
-
import { getProcess
|
|
4
|
+
import { getProcess } from '../../../../../src/db';
|
|
5
|
+
import { isProcessRunning, terminateProcess } from '../../../../../src/platform';
|
|
6
|
+
import { measure } from 'measure-fn';
|
|
5
7
|
|
|
6
8
|
export async function POST(req: Request, { params }: { params: { name: string } }) {
|
|
7
9
|
const name = decodeURIComponent(params.name);
|
|
8
10
|
const proc = getProcess(name);
|
|
9
11
|
|
|
10
|
-
if (!proc
|
|
11
|
-
return Response.json({ error: 'Process not found
|
|
12
|
+
if (!proc) {
|
|
13
|
+
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
await
|
|
16
|
+
const running = await isProcessRunning(proc.pid);
|
|
17
|
+
if (!running) {
|
|
18
|
+
return Response.json({ error: 'Process not running' }, { status: 404 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await measure(`Stop "${name}" (PID ${proc.pid})`, () => terminateProcess(proc.pid));
|
|
15
22
|
return Response.json({ success: true });
|
|
16
23
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* GET /api/version — Return BGR version
|
|
3
3
|
*/
|
|
4
|
-
import { getVersion } from '
|
|
4
|
+
import { getVersion } from '../../../../src/utils';
|
|
5
|
+
import { measure } from 'measure-fn';
|
|
5
6
|
|
|
6
7
|
export async function GET() {
|
|
7
|
-
|
|
8
|
+
const version = await measure('Get version', () => getVersion());
|
|
9
|
+
return Response.json({ version });
|
|
8
10
|
}
|