@appkit/llamacpp-cli 1.12.0 → 1.13.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/README.md +294 -168
- package/dist/cli.js +35 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/launch/claude.d.ts +6 -0
- package/dist/commands/launch/claude.d.ts.map +1 -0
- package/dist/commands/launch/claude.js +277 -0
- package/dist/commands/launch/claude.js.map +1 -0
- package/dist/lib/integration-checker.d.ts +26 -0
- package/dist/lib/integration-checker.d.ts.map +1 -0
- package/dist/lib/integration-checker.js +77 -0
- package/dist/lib/integration-checker.js.map +1 -0
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +10 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +13 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +267 -7
- package/dist/lib/router-server.js.map +1 -1
- package/dist/types/integration-config.d.ts +28 -0
- package/dist/types/integration-config.d.ts.map +1 -0
- package/dist/types/integration-config.js +3 -0
- package/dist/types/integration-config.js.map +1 -0
- package/package.json +10 -2
- package/web/dist/assets/index-Bin89Lwr.css +1 -0
- package/web/dist/assets/index-CVmonw3T.js +17 -0
- package/web/{index.html → dist/index.html} +2 -1
- package/.versionrc.json +0 -16
- package/CHANGELOG.md +0 -213
- package/docs/images/.gitkeep +0 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/src/cli.ts +0 -523
- package/src/commands/admin/config.ts +0 -121
- package/src/commands/admin/logs.ts +0 -91
- package/src/commands/admin/restart.ts +0 -26
- package/src/commands/admin/start.ts +0 -27
- package/src/commands/admin/status.ts +0 -84
- package/src/commands/admin/stop.ts +0 -16
- package/src/commands/config-global.ts +0 -38
- package/src/commands/config.ts +0 -323
- package/src/commands/create.ts +0 -183
- package/src/commands/delete.ts +0 -74
- package/src/commands/list.ts +0 -37
- package/src/commands/logs-all.ts +0 -251
- package/src/commands/logs.ts +0 -345
- package/src/commands/monitor.ts +0 -110
- package/src/commands/ps.ts +0 -84
- package/src/commands/pull.ts +0 -44
- package/src/commands/rm.ts +0 -107
- package/src/commands/router/config.ts +0 -116
- package/src/commands/router/logs.ts +0 -256
- package/src/commands/router/restart.ts +0 -36
- package/src/commands/router/start.ts +0 -60
- package/src/commands/router/status.ts +0 -119
- package/src/commands/router/stop.ts +0 -33
- package/src/commands/run.ts +0 -233
- package/src/commands/search.ts +0 -107
- package/src/commands/server-show.ts +0 -161
- package/src/commands/show.ts +0 -207
- package/src/commands/start.ts +0 -101
- package/src/commands/stop.ts +0 -39
- package/src/commands/tui.ts +0 -25
- package/src/lib/admin-manager.ts +0 -435
- package/src/lib/admin-server.ts +0 -1243
- package/src/lib/config-generator.ts +0 -130
- package/src/lib/download-job-manager.ts +0 -213
- package/src/lib/history-manager.ts +0 -172
- package/src/lib/launchctl-manager.ts +0 -225
- package/src/lib/metrics-aggregator.ts +0 -257
- package/src/lib/model-downloader.ts +0 -328
- package/src/lib/model-scanner.ts +0 -157
- package/src/lib/model-search.ts +0 -114
- package/src/lib/models-dir-setup.ts +0 -46
- package/src/lib/port-manager.ts +0 -80
- package/src/lib/router-logger.ts +0 -201
- package/src/lib/router-manager.ts +0 -414
- package/src/lib/router-server.ts +0 -538
- package/src/lib/state-manager.ts +0 -206
- package/src/lib/status-checker.ts +0 -113
- package/src/lib/system-collector.ts +0 -315
- package/src/tui/ConfigApp.ts +0 -1085
- package/src/tui/HistoricalMonitorApp.ts +0 -587
- package/src/tui/ModelsApp.ts +0 -368
- package/src/tui/MonitorApp.ts +0 -386
- package/src/tui/MultiServerMonitorApp.ts +0 -1833
- package/src/tui/RootNavigator.ts +0 -74
- package/src/tui/SearchApp.ts +0 -511
- package/src/tui/SplashScreen.ts +0 -149
- package/src/types/admin-config.ts +0 -25
- package/src/types/global-config.ts +0 -26
- package/src/types/history-types.ts +0 -39
- package/src/types/model-info.ts +0 -8
- package/src/types/monitor-types.ts +0 -162
- package/src/types/router-config.ts +0 -25
- package/src/types/server-config.ts +0 -46
- package/src/utils/downsample-utils.ts +0 -128
- package/src/utils/file-utils.ts +0 -146
- package/src/utils/format-utils.ts +0 -98
- package/src/utils/log-parser.ts +0 -284
- package/src/utils/log-utils.ts +0 -178
- package/src/utils/process-utils.ts +0 -316
- package/src/utils/prompt-utils.ts +0 -47
- package/test-load.sh +0 -100
- package/tsconfig.json +0 -20
- package/web/eslint.config.js +0 -23
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +0 -4017
- package/web/package.json +0 -38
- package/web/postcss.config.js +0 -6
- package/web/src/App.css +0 -42
- package/web/src/App.tsx +0 -86
- package/web/src/assets/react.svg +0 -1
- package/web/src/components/ApiKeyPrompt.tsx +0 -71
- package/web/src/components/CreateServerModal.tsx +0 -372
- package/web/src/components/DownloadProgress.tsx +0 -123
- package/web/src/components/Nav.tsx +0 -89
- package/web/src/components/RouterConfigModal.tsx +0 -240
- package/web/src/components/SearchModal.tsx +0 -306
- package/web/src/components/ServerConfigModal.tsx +0 -291
- package/web/src/hooks/useApi.ts +0 -259
- package/web/src/index.css +0 -42
- package/web/src/lib/api.ts +0 -226
- package/web/src/main.tsx +0 -10
- package/web/src/pages/Dashboard.tsx +0 -103
- package/web/src/pages/Models.tsx +0 -258
- package/web/src/pages/Router.tsx +0 -270
- package/web/src/pages/RouterLogs.tsx +0 -201
- package/web/src/pages/ServerLogs.tsx +0 -553
- package/web/src/pages/Servers.tsx +0 -358
- package/web/src/types/api.ts +0 -140
- package/web/tailwind.config.js +0 -31
- package/web/tsconfig.app.json +0 -28
- package/web/tsconfig.json +0 -7
- package/web/tsconfig.node.json +0 -26
- package/web/vite.config.ts +0 -25
- /package/web/{public → dist}/vite.svg +0 -0
package/src/lib/admin-server.ts
DELETED
|
@@ -1,1243 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import * as http from 'http';
|
|
4
|
-
import { URL } from 'url';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as fs from 'fs/promises';
|
|
7
|
-
import { AdminConfig } from '../types/admin-config';
|
|
8
|
-
import { ServerConfig } from '../types/server-config';
|
|
9
|
-
import { readJson, fileExists, getConfigDir, getServersDir } from '../utils/file-utils';
|
|
10
|
-
import { stateManager } from './state-manager';
|
|
11
|
-
import { launchctlManager } from './launchctl-manager';
|
|
12
|
-
import { modelScanner } from './model-scanner';
|
|
13
|
-
import { configGenerator } from './config-generator';
|
|
14
|
-
import { portManager } from './port-manager';
|
|
15
|
-
import { statusChecker } from './status-checker';
|
|
16
|
-
import { modelDownloader } from './model-downloader';
|
|
17
|
-
import { modelSearch } from './model-search';
|
|
18
|
-
import { downloadJobManager } from './download-job-manager';
|
|
19
|
-
import { routerManager } from './router-manager';
|
|
20
|
-
|
|
21
|
-
interface ErrorResponse {
|
|
22
|
-
error: string;
|
|
23
|
-
details?: string;
|
|
24
|
-
code?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface SuccessResponse {
|
|
28
|
-
success: boolean;
|
|
29
|
-
[key: string]: any;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Admin HTTP server - REST API for managing llama.cpp servers
|
|
34
|
-
*/
|
|
35
|
-
class AdminServer {
|
|
36
|
-
private config!: AdminConfig;
|
|
37
|
-
private server!: http.Server;
|
|
38
|
-
|
|
39
|
-
async initialize(): Promise<void> {
|
|
40
|
-
// Load admin config
|
|
41
|
-
const configPath = path.join(getConfigDir(), 'admin.json');
|
|
42
|
-
if (!(await fileExists(configPath))) {
|
|
43
|
-
throw new Error('Admin configuration not found');
|
|
44
|
-
}
|
|
45
|
-
this.config = await readJson<AdminConfig>(configPath);
|
|
46
|
-
|
|
47
|
-
// Create HTTP server
|
|
48
|
-
this.server = http.createServer(async (req, res) => {
|
|
49
|
-
await this.handleRequest(req, res);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Graceful shutdown
|
|
53
|
-
process.on('SIGTERM', async () => {
|
|
54
|
-
console.error('[Admin] Received SIGTERM, shutting down gracefully...');
|
|
55
|
-
this.server.close(() => {
|
|
56
|
-
console.error('[Admin] Server closed');
|
|
57
|
-
process.exit(0);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
process.on('SIGINT', async () => {
|
|
62
|
-
console.error('[Admin] Received SIGINT, shutting down gracefully...');
|
|
63
|
-
this.server.close(() => {
|
|
64
|
-
console.error('[Admin] Server closed');
|
|
65
|
-
process.exit(0);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async start(): Promise<void> {
|
|
71
|
-
await this.initialize();
|
|
72
|
-
|
|
73
|
-
this.server.listen(this.config.port, this.config.host, () => {
|
|
74
|
-
console.error(`[Admin] Listening on http://${this.config.host}:${this.config.port}`);
|
|
75
|
-
console.error(`[Admin] PID: ${process.pid}`);
|
|
76
|
-
console.error(`[Admin] API Key: ${this.config.apiKey}`);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Main request handler
|
|
82
|
-
*/
|
|
83
|
-
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
84
|
-
// CORS headers
|
|
85
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
86
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
87
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
88
|
-
|
|
89
|
-
// Handle OPTIONS preflight
|
|
90
|
-
if (req.method === 'OPTIONS') {
|
|
91
|
-
res.writeHead(200);
|
|
92
|
-
res.end();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const startTime = Date.now();
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
100
|
-
const pathname = url.pathname;
|
|
101
|
-
const method = req.method!;
|
|
102
|
-
|
|
103
|
-
// Log request
|
|
104
|
-
this.logRequest(method, pathname);
|
|
105
|
-
|
|
106
|
-
// Health endpoint (no auth required)
|
|
107
|
-
if (pathname === '/health' && method === 'GET') {
|
|
108
|
-
await this.handleHealth(req, res);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Static files (no auth required)
|
|
113
|
-
if (!pathname.startsWith('/api/')) {
|
|
114
|
-
await this.handleStaticFile(req, res, pathname);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Authenticate API endpoints
|
|
119
|
-
if (!this.authenticate(req)) {
|
|
120
|
-
this.sendError(res, 401, 'Unauthorized', 'Invalid or missing API key', 'UNAUTHORIZED');
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Route based on path and method
|
|
125
|
-
if (pathname === '/api/servers' && method === 'GET') {
|
|
126
|
-
await this.handleListServers(req, res);
|
|
127
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+$/) && method === 'GET') {
|
|
128
|
-
const serverId = pathname.split('/').pop()!;
|
|
129
|
-
await this.handleGetServer(req, res, serverId);
|
|
130
|
-
} else if (pathname === '/api/servers' && method === 'POST') {
|
|
131
|
-
await this.handleCreateServer(req, res);
|
|
132
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+$/) && method === 'PATCH') {
|
|
133
|
-
const serverId = pathname.split('/').pop()!;
|
|
134
|
-
await this.handleUpdateServer(req, res, serverId);
|
|
135
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+$/) && method === 'DELETE') {
|
|
136
|
-
const serverId = pathname.split('/').pop()!;
|
|
137
|
-
await this.handleDeleteServer(req, res, serverId);
|
|
138
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+\/start$/) && method === 'POST') {
|
|
139
|
-
const serverId = pathname.split('/')[3];
|
|
140
|
-
await this.handleStartServer(req, res, serverId);
|
|
141
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+\/stop$/) && method === 'POST') {
|
|
142
|
-
const serverId = pathname.split('/')[3];
|
|
143
|
-
await this.handleStopServer(req, res, serverId);
|
|
144
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+\/restart$/) && method === 'POST') {
|
|
145
|
-
const serverId = pathname.split('/')[3];
|
|
146
|
-
await this.handleRestartServer(req, res, serverId);
|
|
147
|
-
} else if (pathname.match(/^\/api\/servers\/[^/]+\/logs$/) && method === 'GET') {
|
|
148
|
-
const serverId = pathname.split('/')[3];
|
|
149
|
-
await this.handleGetLogs(req, res, serverId, url);
|
|
150
|
-
} else if (pathname === '/api/models' && method === 'GET') {
|
|
151
|
-
await this.handleListModels(req, res);
|
|
152
|
-
} else if (pathname === '/api/models/search' && method === 'GET') {
|
|
153
|
-
await this.handleSearchModels(req, res, url);
|
|
154
|
-
} else if (pathname === '/api/models/download' && method === 'POST') {
|
|
155
|
-
await this.handleDownloadModel(req, res);
|
|
156
|
-
} else if (pathname.match(/^\/api\/models\/[^/]+\/files$/) && method === 'GET') {
|
|
157
|
-
// Extract repo ID (everything between /api/models/ and /files)
|
|
158
|
-
const match = pathname.match(/^\/api\/models\/(.+)\/files$/);
|
|
159
|
-
const repoId = decodeURIComponent(match![1]);
|
|
160
|
-
await this.handleGetModelFiles(req, res, repoId);
|
|
161
|
-
} else if (pathname === '/api/jobs' && method === 'GET') {
|
|
162
|
-
await this.handleListJobs(req, res);
|
|
163
|
-
} else if (pathname.match(/^\/api\/jobs\/[^/]+$/) && method === 'GET') {
|
|
164
|
-
const jobId = pathname.split('/').pop()!;
|
|
165
|
-
await this.handleGetJob(req, res, jobId);
|
|
166
|
-
} else if (pathname.match(/^\/api\/jobs\/[^/]+$/) && method === 'DELETE') {
|
|
167
|
-
const jobId = pathname.split('/').pop()!;
|
|
168
|
-
await this.handleCancelJob(req, res, jobId);
|
|
169
|
-
} else if (pathname.match(/^\/api\/models\/[^/]+$/) && method === 'GET') {
|
|
170
|
-
const modelName = decodeURIComponent(pathname.split('/').pop()!);
|
|
171
|
-
await this.handleGetModel(req, res, modelName);
|
|
172
|
-
} else if (pathname.match(/^\/api\/models\/[^/]+$/) && method === 'DELETE') {
|
|
173
|
-
const modelName = decodeURIComponent(pathname.split('/').pop()!);
|
|
174
|
-
await this.handleDeleteModel(req, res, modelName, url);
|
|
175
|
-
} else if (pathname === '/api/status' && method === 'GET') {
|
|
176
|
-
await this.handleSystemStatus(req, res);
|
|
177
|
-
} else if (pathname === '/api/router' && method === 'GET') {
|
|
178
|
-
await this.handleGetRouter(req, res);
|
|
179
|
-
} else if (pathname === '/api/router/start' && method === 'POST') {
|
|
180
|
-
await this.handleStartRouter(req, res);
|
|
181
|
-
} else if (pathname === '/api/router/stop' && method === 'POST') {
|
|
182
|
-
await this.handleStopRouter(req, res);
|
|
183
|
-
} else if (pathname === '/api/router/restart' && method === 'POST') {
|
|
184
|
-
await this.handleRestartRouter(req, res);
|
|
185
|
-
} else if (pathname === '/api/router/logs' && method === 'GET') {
|
|
186
|
-
await this.handleGetRouterLogs(req, res, url);
|
|
187
|
-
} else if (pathname === '/api/router' && method === 'PATCH') {
|
|
188
|
-
await this.handleUpdateRouter(req, res);
|
|
189
|
-
} else {
|
|
190
|
-
// API endpoint not found
|
|
191
|
-
this.sendError(res, 404, 'Not Found', `Unknown endpoint: ${method} ${pathname}`, 'NOT_FOUND');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Log response time
|
|
195
|
-
const duration = Date.now() - startTime;
|
|
196
|
-
this.logResponse(method, pathname, res.statusCode, duration);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error('[Admin] Error handling request:', error);
|
|
199
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'INTERNAL_ERROR');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Health check endpoint
|
|
205
|
-
*/
|
|
206
|
-
private async handleHealth(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
207
|
-
this.sendJson(res, 200, {
|
|
208
|
-
status: 'healthy',
|
|
209
|
-
uptime: process.uptime(),
|
|
210
|
-
timestamp: new Date().toISOString(),
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* List all servers
|
|
216
|
-
*/
|
|
217
|
-
private async handleListServers(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
218
|
-
const servers = await stateManager.getAllServers();
|
|
219
|
-
|
|
220
|
-
// Update status for each server
|
|
221
|
-
for (const server of servers) {
|
|
222
|
-
const status = await statusChecker.checkServer(server);
|
|
223
|
-
server.status = statusChecker.determineStatus(status, status.portListening);
|
|
224
|
-
server.pid = status.pid || undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
this.sendJson(res, 200, { servers });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get server details
|
|
232
|
-
*/
|
|
233
|
-
private async handleGetServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
234
|
-
const server = await stateManager.findServer(serverId);
|
|
235
|
-
if (!server) {
|
|
236
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const status = await statusChecker.checkServer(server);
|
|
241
|
-
server.status = statusChecker.determineStatus(status, status.portListening);
|
|
242
|
-
server.pid = status.pid || undefined;
|
|
243
|
-
|
|
244
|
-
this.sendJson(res, 200, { server, status });
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Create new server
|
|
249
|
-
*/
|
|
250
|
-
private async handleCreateServer(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
251
|
-
const body = await this.readBody(req);
|
|
252
|
-
let data: any;
|
|
253
|
-
try {
|
|
254
|
-
data = JSON.parse(body);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
this.sendError(res, 400, 'Bad Request', 'Invalid JSON in request body', 'INVALID_JSON');
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Validate required fields
|
|
261
|
-
if (!data.model) {
|
|
262
|
-
this.sendError(res, 400, 'Bad Request', 'Missing required field: model', 'MISSING_FIELD');
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
try {
|
|
267
|
-
// Resolve model path
|
|
268
|
-
const modelPath = await modelScanner.resolveModelPath(data.model);
|
|
269
|
-
if (!modelPath) {
|
|
270
|
-
this.sendError(res, 404, 'Not Found', `Model not found: ${data.model}`, 'MODEL_NOT_FOUND');
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const modelName = path.basename(modelPath);
|
|
275
|
-
|
|
276
|
-
// Check if server already exists
|
|
277
|
-
const existingServer = await stateManager.serverExistsForModel(modelPath);
|
|
278
|
-
if (existingServer) {
|
|
279
|
-
this.sendError(res, 409, 'Conflict', `Server already exists for model: ${modelName}`, 'SERVER_EXISTS');
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Get model size
|
|
284
|
-
const modelSize = await modelScanner.getModelSize(modelName);
|
|
285
|
-
if (!modelSize) {
|
|
286
|
-
this.sendError(res, 500, 'Internal Server Error', 'Failed to read model file', 'MODEL_READ_ERROR');
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Determine port
|
|
291
|
-
let port: number;
|
|
292
|
-
if (data.port) {
|
|
293
|
-
portManager.validatePort(data.port);
|
|
294
|
-
const available = await portManager.isPortAvailable(data.port);
|
|
295
|
-
if (!available) {
|
|
296
|
-
this.sendError(res, 409, 'Conflict', `Port ${data.port} is already in use`, 'PORT_IN_USE');
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
port = data.port;
|
|
300
|
-
} else {
|
|
301
|
-
port = await portManager.findAvailablePort();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Parse custom flags if provided
|
|
305
|
-
let customFlags: string[] | undefined;
|
|
306
|
-
if (data.customFlags) {
|
|
307
|
-
customFlags = Array.isArray(data.customFlags)
|
|
308
|
-
? data.customFlags
|
|
309
|
-
: data.customFlags.split(',').map((f: string) => f.trim()).filter((f: string) => f.length > 0);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Generate configuration
|
|
313
|
-
const serverConfig = await configGenerator.generateConfig(
|
|
314
|
-
modelPath,
|
|
315
|
-
modelName,
|
|
316
|
-
modelSize,
|
|
317
|
-
port,
|
|
318
|
-
{
|
|
319
|
-
port: data.port,
|
|
320
|
-
host: data.host,
|
|
321
|
-
threads: data.threads,
|
|
322
|
-
ctxSize: data.ctxSize,
|
|
323
|
-
gpuLayers: data.gpuLayers,
|
|
324
|
-
verbose: data.verbose,
|
|
325
|
-
customFlags,
|
|
326
|
-
}
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
// Save configuration
|
|
330
|
-
await stateManager.saveServerConfig(serverConfig);
|
|
331
|
-
|
|
332
|
-
// Create and start server
|
|
333
|
-
await launchctlManager.createPlist(serverConfig);
|
|
334
|
-
await launchctlManager.loadService(serverConfig.plistPath);
|
|
335
|
-
await launchctlManager.startService(serverConfig.label);
|
|
336
|
-
|
|
337
|
-
// Wait for startup
|
|
338
|
-
const started = await launchctlManager.waitForServiceStart(serverConfig.label, 5000);
|
|
339
|
-
if (!started) {
|
|
340
|
-
this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Update status
|
|
345
|
-
const status = await statusChecker.checkServer(serverConfig);
|
|
346
|
-
serverConfig.status = statusChecker.determineStatus(status, status.portListening);
|
|
347
|
-
serverConfig.pid = status.pid || undefined;
|
|
348
|
-
await stateManager.updateServerConfig(serverConfig.id, {
|
|
349
|
-
status: serverConfig.status,
|
|
350
|
-
pid: serverConfig.pid,
|
|
351
|
-
lastStarted: new Date().toISOString(),
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
this.sendJson(res, 201, { server: serverConfig });
|
|
355
|
-
} catch (error) {
|
|
356
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'CREATE_ERROR');
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Update server configuration
|
|
362
|
-
*/
|
|
363
|
-
private async handleUpdateServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
364
|
-
const server = await stateManager.findServer(serverId);
|
|
365
|
-
if (!server) {
|
|
366
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const body = await this.readBody(req);
|
|
371
|
-
let data: any;
|
|
372
|
-
try {
|
|
373
|
-
data = JSON.parse(body);
|
|
374
|
-
} catch (error) {
|
|
375
|
-
this.sendError(res, 400, 'Bad Request', 'Invalid JSON in request body', 'INVALID_JSON');
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
// Build updates object
|
|
381
|
-
const updates: Partial<ServerConfig> = {};
|
|
382
|
-
|
|
383
|
-
if (data.model !== undefined) {
|
|
384
|
-
const modelPath = await modelScanner.resolveModelPath(data.model);
|
|
385
|
-
if (!modelPath) {
|
|
386
|
-
this.sendError(res, 404, 'Not Found', `Model not found: ${data.model}`, 'MODEL_NOT_FOUND');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
updates.modelPath = modelPath;
|
|
390
|
-
updates.modelName = path.basename(modelPath);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (data.port !== undefined) {
|
|
394
|
-
portManager.validatePort(data.port);
|
|
395
|
-
const available = await portManager.isPortAvailable(data.port);
|
|
396
|
-
if (!available && data.port !== server.port) {
|
|
397
|
-
this.sendError(res, 409, 'Conflict', `Port ${data.port} is already in use`, 'PORT_IN_USE');
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
updates.port = data.port;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (data.host !== undefined) updates.host = data.host;
|
|
404
|
-
if (data.threads !== undefined) updates.threads = data.threads;
|
|
405
|
-
if (data.ctxSize !== undefined) updates.ctxSize = data.ctxSize;
|
|
406
|
-
if (data.gpuLayers !== undefined) updates.gpuLayers = data.gpuLayers;
|
|
407
|
-
if (data.verbose !== undefined) updates.verbose = data.verbose;
|
|
408
|
-
if (data.customFlags !== undefined) {
|
|
409
|
-
updates.customFlags = Array.isArray(data.customFlags)
|
|
410
|
-
? data.customFlags
|
|
411
|
-
: data.customFlags.split(',').map((f: string) => f.trim()).filter((f: string) => f.length > 0);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Check if server is running
|
|
415
|
-
const status = await statusChecker.checkServer(server);
|
|
416
|
-
const isRunning = statusChecker.determineStatus(status, status.portListening) === 'running';
|
|
417
|
-
|
|
418
|
-
// Apply updates
|
|
419
|
-
await stateManager.updateServerConfig(server.id, updates);
|
|
420
|
-
|
|
421
|
-
// Regenerate plist with new config
|
|
422
|
-
const updatedServer = await stateManager.loadServerConfig(server.id);
|
|
423
|
-
if (updatedServer) {
|
|
424
|
-
await launchctlManager.createPlist(updatedServer);
|
|
425
|
-
|
|
426
|
-
// Restart if requested and running
|
|
427
|
-
if (data.restart && isRunning) {
|
|
428
|
-
await launchctlManager.unloadService(updatedServer.plistPath);
|
|
429
|
-
await launchctlManager.loadService(updatedServer.plistPath);
|
|
430
|
-
await launchctlManager.startService(updatedServer.label);
|
|
431
|
-
await launchctlManager.waitForServiceStart(updatedServer.label, 5000);
|
|
432
|
-
|
|
433
|
-
const newStatus = await statusChecker.checkServer(updatedServer);
|
|
434
|
-
await stateManager.updateServerConfig(updatedServer.id, {
|
|
435
|
-
status: statusChecker.determineStatus(newStatus, newStatus.portListening),
|
|
436
|
-
pid: newStatus.pid || undefined,
|
|
437
|
-
lastStarted: new Date().toISOString(),
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const finalServer = await stateManager.loadServerConfig(server.id);
|
|
443
|
-
this.sendJson(res, 200, { server: finalServer });
|
|
444
|
-
} catch (error) {
|
|
445
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'UPDATE_ERROR');
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/**
|
|
450
|
-
* Delete server
|
|
451
|
-
*/
|
|
452
|
-
private async handleDeleteServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
453
|
-
const server = await stateManager.findServer(serverId);
|
|
454
|
-
if (!server) {
|
|
455
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
// Stop server if running
|
|
461
|
-
const status = await statusChecker.checkServer(server);
|
|
462
|
-
if (statusChecker.determineStatus(status, status.portListening) === 'running') {
|
|
463
|
-
await launchctlManager.unloadService(server.plistPath);
|
|
464
|
-
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Delete plist and config
|
|
468
|
-
await launchctlManager.deletePlist(server.plistPath);
|
|
469
|
-
await stateManager.deleteServerConfig(server.id);
|
|
470
|
-
|
|
471
|
-
this.sendJson(res, 200, { success: true });
|
|
472
|
-
} catch (error) {
|
|
473
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'DELETE_ERROR');
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Start server
|
|
479
|
-
*/
|
|
480
|
-
private async handleStartServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
481
|
-
const server = await stateManager.findServer(serverId);
|
|
482
|
-
if (!server) {
|
|
483
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
try {
|
|
488
|
-
const status = await statusChecker.checkServer(server);
|
|
489
|
-
if (statusChecker.determineStatus(status, status.portListening) === 'running') {
|
|
490
|
-
this.sendError(res, 409, 'Conflict', 'Server is already running', 'ALREADY_RUNNING');
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Recreate plist if missing
|
|
495
|
-
if (!(await fileExists(server.plistPath))) {
|
|
496
|
-
await launchctlManager.createPlist(server);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
await launchctlManager.loadService(server.plistPath);
|
|
500
|
-
await launchctlManager.startService(server.label);
|
|
501
|
-
const started = await launchctlManager.waitForServiceStart(server.label, 5000);
|
|
502
|
-
|
|
503
|
-
if (!started) {
|
|
504
|
-
this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Give server a moment to fully start before checking status
|
|
509
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
510
|
-
|
|
511
|
-
const newStatus = await statusChecker.checkServer(server);
|
|
512
|
-
await stateManager.updateServerConfig(server.id, {
|
|
513
|
-
status: statusChecker.determineStatus(newStatus, newStatus.portListening),
|
|
514
|
-
pid: newStatus.pid || undefined,
|
|
515
|
-
lastStarted: new Date().toISOString(),
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
const updatedServer = await stateManager.loadServerConfig(server.id);
|
|
519
|
-
this.sendJson(res, 200, { server: updatedServer });
|
|
520
|
-
} catch (error) {
|
|
521
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'START_ERROR');
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Stop server
|
|
527
|
-
*/
|
|
528
|
-
private async handleStopServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
529
|
-
const server = await stateManager.findServer(serverId);
|
|
530
|
-
if (!server) {
|
|
531
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
try {
|
|
536
|
-
const status = await statusChecker.checkServer(server);
|
|
537
|
-
if (statusChecker.determineStatus(status, status.portListening) !== 'running') {
|
|
538
|
-
this.sendError(res, 409, 'Conflict', 'Server is not running', 'NOT_RUNNING');
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
await launchctlManager.unloadService(server.plistPath);
|
|
543
|
-
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
544
|
-
|
|
545
|
-
await stateManager.updateServerConfig(server.id, {
|
|
546
|
-
status: 'stopped',
|
|
547
|
-
pid: undefined,
|
|
548
|
-
lastStopped: new Date().toISOString(),
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
const updatedServer = await stateManager.loadServerConfig(server.id);
|
|
552
|
-
this.sendJson(res, 200, { server: updatedServer });
|
|
553
|
-
} catch (error) {
|
|
554
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'STOP_ERROR');
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Restart server
|
|
560
|
-
*/
|
|
561
|
-
private async handleRestartServer(req: http.IncomingMessage, res: http.ServerResponse, serverId: string): Promise<void> {
|
|
562
|
-
const server = await stateManager.findServer(serverId);
|
|
563
|
-
if (!server) {
|
|
564
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
try {
|
|
569
|
-
// Stop if running
|
|
570
|
-
const status = await statusChecker.checkServer(server);
|
|
571
|
-
if (statusChecker.determineStatus(status, status.portListening) === 'running') {
|
|
572
|
-
await launchctlManager.unloadService(server.plistPath);
|
|
573
|
-
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Start
|
|
577
|
-
await launchctlManager.loadService(server.plistPath);
|
|
578
|
-
await launchctlManager.startService(server.label);
|
|
579
|
-
const started = await launchctlManager.waitForServiceStart(server.label, 5000);
|
|
580
|
-
|
|
581
|
-
if (!started) {
|
|
582
|
-
this.sendError(res, 500, 'Internal Server Error', 'Server failed to start', 'START_FAILED');
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// Give server a moment to fully start before checking status
|
|
587
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
588
|
-
|
|
589
|
-
const newStatus = await statusChecker.checkServer(server);
|
|
590
|
-
await stateManager.updateServerConfig(server.id, {
|
|
591
|
-
status: statusChecker.determineStatus(newStatus, newStatus.portListening),
|
|
592
|
-
pid: newStatus.pid || undefined,
|
|
593
|
-
lastStarted: new Date().toISOString(),
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
const updatedServer = await stateManager.loadServerConfig(server.id);
|
|
597
|
-
this.sendJson(res, 200, { server: updatedServer });
|
|
598
|
-
} catch (error) {
|
|
599
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'RESTART_ERROR');
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Get server logs
|
|
605
|
-
*/
|
|
606
|
-
private async handleGetLogs(req: http.IncomingMessage, res: http.ServerResponse, serverId: string, url: URL): Promise<void> {
|
|
607
|
-
const server = await stateManager.findServer(serverId);
|
|
608
|
-
if (!server) {
|
|
609
|
-
this.sendError(res, 404, 'Not Found', `Server not found: ${serverId}`, 'SERVER_NOT_FOUND');
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
try {
|
|
614
|
-
const type = url.searchParams.get('type') || 'both'; // stdout, stderr, or both
|
|
615
|
-
const lines = parseInt(url.searchParams.get('lines') || '100');
|
|
616
|
-
|
|
617
|
-
let stdout = '';
|
|
618
|
-
let stderr = '';
|
|
619
|
-
|
|
620
|
-
if ((type === 'stdout' || type === 'both') && (await fileExists(server.stdoutPath))) {
|
|
621
|
-
const content = await fs.readFile(server.stdoutPath, 'utf-8');
|
|
622
|
-
const logLines = content.split('\n');
|
|
623
|
-
stdout = logLines.slice(-lines).join('\n');
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
if ((type === 'stderr' || type === 'both') && (await fileExists(server.stderrPath))) {
|
|
627
|
-
const content = await fs.readFile(server.stderrPath, 'utf-8');
|
|
628
|
-
const logLines = content.split('\n');
|
|
629
|
-
stderr = logLines.slice(-lines).join('\n');
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
this.sendJson(res, 200, { stdout, stderr });
|
|
633
|
-
} catch (error) {
|
|
634
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'LOGS_ERROR');
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* List models
|
|
640
|
-
*/
|
|
641
|
-
private async handleListModels(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
642
|
-
try {
|
|
643
|
-
const models = await modelScanner.scanModels();
|
|
644
|
-
const modelsWithServers = await Promise.all(
|
|
645
|
-
models.map(async (model) => {
|
|
646
|
-
const servers = await stateManager.getAllServers();
|
|
647
|
-
const usingServers = servers.filter(s => s.modelName === model.filename);
|
|
648
|
-
return {
|
|
649
|
-
...model,
|
|
650
|
-
serversUsing: usingServers.length,
|
|
651
|
-
serverIds: usingServers.map(s => s.id),
|
|
652
|
-
};
|
|
653
|
-
})
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
this.sendJson(res, 200, { models: modelsWithServers });
|
|
657
|
-
} catch (error) {
|
|
658
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'LIST_MODELS_ERROR');
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Get model details
|
|
664
|
-
*/
|
|
665
|
-
private async handleGetModel(req: http.IncomingMessage, res: http.ServerResponse, modelName: string): Promise<void> {
|
|
666
|
-
try {
|
|
667
|
-
const models = await modelScanner.scanModels();
|
|
668
|
-
const model = models.find(m => m.filename === modelName);
|
|
669
|
-
|
|
670
|
-
if (!model) {
|
|
671
|
-
this.sendError(res, 404, 'Not Found', `Model not found: ${modelName}`, 'MODEL_NOT_FOUND');
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const servers = await stateManager.getAllServers();
|
|
676
|
-
const usingServers = servers.filter(s => s.modelName === modelName);
|
|
677
|
-
|
|
678
|
-
this.sendJson(res, 200, {
|
|
679
|
-
model: {
|
|
680
|
-
...model,
|
|
681
|
-
serversUsing: usingServers.length,
|
|
682
|
-
serverIds: usingServers.map(s => s.id),
|
|
683
|
-
},
|
|
684
|
-
});
|
|
685
|
-
} catch (error) {
|
|
686
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'GET_MODEL_ERROR');
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Search models on HuggingFace
|
|
692
|
-
*/
|
|
693
|
-
private async handleSearchModels(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> {
|
|
694
|
-
const query = url.searchParams.get('q');
|
|
695
|
-
if (!query) {
|
|
696
|
-
this.sendError(res, 400, 'Bad Request', 'Missing query parameter: q', 'MISSING_QUERY');
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
const results = await modelSearch.searchModels(query, limit);
|
|
704
|
-
this.sendJson(res, 200, { results });
|
|
705
|
-
} catch (error) {
|
|
706
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'SEARCH_ERROR');
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Get GGUF files for a HuggingFace model
|
|
712
|
-
*/
|
|
713
|
-
private async handleGetModelFiles(req: http.IncomingMessage, res: http.ServerResponse, repoId: string): Promise<void> {
|
|
714
|
-
try {
|
|
715
|
-
const files = await modelSearch.getModelFiles(repoId);
|
|
716
|
-
this.sendJson(res, 200, { repoId, files });
|
|
717
|
-
} catch (error) {
|
|
718
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'GET_FILES_ERROR');
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
/**
|
|
723
|
-
* List all download jobs
|
|
724
|
-
*/
|
|
725
|
-
private async handleListJobs(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
726
|
-
const jobs = downloadJobManager.listJobs();
|
|
727
|
-
this.sendJson(res, 200, { jobs });
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Get a specific download job
|
|
732
|
-
*/
|
|
733
|
-
private async handleGetJob(req: http.IncomingMessage, res: http.ServerResponse, jobId: string): Promise<void> {
|
|
734
|
-
const job = downloadJobManager.getJob(jobId);
|
|
735
|
-
if (!job) {
|
|
736
|
-
this.sendError(res, 404, 'Not Found', `Job not found: ${jobId}`, 'JOB_NOT_FOUND');
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
this.sendJson(res, 200, { job });
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Cancel a download job
|
|
744
|
-
*/
|
|
745
|
-
private async handleCancelJob(req: http.IncomingMessage, res: http.ServerResponse, jobId: string): Promise<void> {
|
|
746
|
-
const job = downloadJobManager.getJob(jobId);
|
|
747
|
-
if (!job) {
|
|
748
|
-
this.sendError(res, 404, 'Not Found', `Job not found: ${jobId}`, 'JOB_NOT_FOUND');
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
753
|
-
// Job already finished, just delete it
|
|
754
|
-
downloadJobManager.deleteJob(jobId);
|
|
755
|
-
this.sendJson(res, 200, { success: true, message: 'Job removed' });
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
const cancelled = downloadJobManager.cancelJob(jobId);
|
|
760
|
-
if (cancelled) {
|
|
761
|
-
this.sendJson(res, 200, { success: true, message: 'Job cancelled' });
|
|
762
|
-
} else {
|
|
763
|
-
this.sendError(res, 400, 'Bad Request', 'Cannot cancel job', 'CANCEL_FAILED');
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Download model from HuggingFace
|
|
769
|
-
*/
|
|
770
|
-
private async handleDownloadModel(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
771
|
-
const body = await this.readBody(req);
|
|
772
|
-
let data: any;
|
|
773
|
-
try {
|
|
774
|
-
data = JSON.parse(body);
|
|
775
|
-
} catch (error) {
|
|
776
|
-
this.sendError(res, 400, 'Bad Request', 'Invalid JSON in request body', 'INVALID_JSON');
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (!data.repo || !data.filename) {
|
|
781
|
-
this.sendError(res, 400, 'Bad Request', 'Missing required fields: repo, filename', 'MISSING_FIELDS');
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
// Create download job (starts download asynchronously)
|
|
787
|
-
const jobId = downloadJobManager.createJob(data.repo, data.filename);
|
|
788
|
-
|
|
789
|
-
this.sendJson(res, 202, {
|
|
790
|
-
jobId,
|
|
791
|
-
status: 'pending',
|
|
792
|
-
repo: data.repo,
|
|
793
|
-
filename: data.filename,
|
|
794
|
-
message: 'Download started. Check status with GET /api/jobs/:jobId',
|
|
795
|
-
});
|
|
796
|
-
} catch (error) {
|
|
797
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'DOWNLOAD_ERROR');
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Delete model
|
|
803
|
-
*/
|
|
804
|
-
private async handleDeleteModel(req: http.IncomingMessage, res: http.ServerResponse, modelName: string, url: URL): Promise<void> {
|
|
805
|
-
try {
|
|
806
|
-
const cascade = url.searchParams.get('cascade') === 'true';
|
|
807
|
-
|
|
808
|
-
// Find servers using this model
|
|
809
|
-
const servers = await stateManager.getAllServers();
|
|
810
|
-
const usingServers = servers.filter(s => s.modelName === modelName);
|
|
811
|
-
|
|
812
|
-
// Block deletion if servers exist and cascade not specified
|
|
813
|
-
if (usingServers.length > 0 && !cascade) {
|
|
814
|
-
this.sendError(
|
|
815
|
-
res,
|
|
816
|
-
409,
|
|
817
|
-
'Conflict',
|
|
818
|
-
`Model is used by ${usingServers.length} server(s). Use ?cascade=true to delete model and servers.`,
|
|
819
|
-
'MODEL_IN_USE'
|
|
820
|
-
);
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Delete servers if cascade
|
|
825
|
-
const deletedServers: string[] = [];
|
|
826
|
-
if (cascade) {
|
|
827
|
-
for (const server of usingServers) {
|
|
828
|
-
const status = await statusChecker.checkServer(server);
|
|
829
|
-
if (statusChecker.determineStatus(status, status.portListening) === 'running') {
|
|
830
|
-
await launchctlManager.unloadService(server.plistPath);
|
|
831
|
-
await launchctlManager.waitForServiceStop(server.label, 5000);
|
|
832
|
-
}
|
|
833
|
-
await launchctlManager.deletePlist(server.plistPath);
|
|
834
|
-
await stateManager.deleteServerConfig(server.id);
|
|
835
|
-
deletedServers.push(server.id);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
// Delete model file
|
|
840
|
-
const modelPath = await modelScanner.resolveModelPath(modelName);
|
|
841
|
-
if (!modelPath) {
|
|
842
|
-
this.sendError(res, 404, 'Not Found', `Model not found: ${modelName}`, 'MODEL_NOT_FOUND');
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
await fs.unlink(modelPath);
|
|
847
|
-
|
|
848
|
-
this.sendJson(res, 200, {
|
|
849
|
-
success: true,
|
|
850
|
-
deletedServers: deletedServers.length > 0 ? deletedServers : undefined,
|
|
851
|
-
});
|
|
852
|
-
} catch (error) {
|
|
853
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'DELETE_MODEL_ERROR');
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
/**
|
|
858
|
-
* Get system status
|
|
859
|
-
*/
|
|
860
|
-
private async handleSystemStatus(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
861
|
-
try {
|
|
862
|
-
const servers = await stateManager.getAllServers();
|
|
863
|
-
const models = await modelScanner.scanModels();
|
|
864
|
-
|
|
865
|
-
// Update server statuses
|
|
866
|
-
for (const server of servers) {
|
|
867
|
-
const status = await statusChecker.checkServer(server);
|
|
868
|
-
server.status = statusChecker.determineStatus(status, status.portListening);
|
|
869
|
-
server.pid = status.pid || undefined;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const runningServers = servers.filter(s => s.status === 'running');
|
|
873
|
-
const stoppedServers = servers.filter(s => s.status === 'stopped');
|
|
874
|
-
const crashedServers = servers.filter(s => s.status === 'crashed');
|
|
875
|
-
|
|
876
|
-
this.sendJson(res, 200, {
|
|
877
|
-
servers: {
|
|
878
|
-
total: servers.length,
|
|
879
|
-
running: runningServers.length,
|
|
880
|
-
stopped: stoppedServers.length,
|
|
881
|
-
crashed: crashedServers.length,
|
|
882
|
-
},
|
|
883
|
-
models: {
|
|
884
|
-
total: models.length,
|
|
885
|
-
totalSize: models.reduce((sum, m) => sum + m.size, 0),
|
|
886
|
-
},
|
|
887
|
-
system: {
|
|
888
|
-
uptime: process.uptime(),
|
|
889
|
-
timestamp: new Date().toISOString(),
|
|
890
|
-
},
|
|
891
|
-
});
|
|
892
|
-
} catch (error) {
|
|
893
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'STATUS_ERROR');
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Get router status
|
|
899
|
-
*/
|
|
900
|
-
private async handleGetRouter(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
901
|
-
try {
|
|
902
|
-
const routerStatus = await routerManager.getStatus();
|
|
903
|
-
|
|
904
|
-
if (!routerStatus) {
|
|
905
|
-
this.sendJson(res, 200, {
|
|
906
|
-
status: 'not_configured',
|
|
907
|
-
config: null,
|
|
908
|
-
isRunning: false,
|
|
909
|
-
});
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
const { config, status } = routerStatus;
|
|
914
|
-
|
|
915
|
-
// Get available models from running servers
|
|
916
|
-
const servers = await stateManager.getAllServers();
|
|
917
|
-
const runningServers = [];
|
|
918
|
-
|
|
919
|
-
for (const server of servers) {
|
|
920
|
-
const serverStatus = await statusChecker.checkServer(server);
|
|
921
|
-
if (statusChecker.determineStatus(serverStatus, serverStatus.portListening) === 'running') {
|
|
922
|
-
runningServers.push({
|
|
923
|
-
id: server.id,
|
|
924
|
-
modelName: server.modelName,
|
|
925
|
-
port: server.port,
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
this.sendJson(res, 200, {
|
|
931
|
-
status: status.isRunning ? 'running' : 'stopped',
|
|
932
|
-
config: {
|
|
933
|
-
port: config.port,
|
|
934
|
-
host: config.host,
|
|
935
|
-
verbose: config.verbose,
|
|
936
|
-
requestTimeout: config.requestTimeout,
|
|
937
|
-
healthCheckInterval: config.healthCheckInterval,
|
|
938
|
-
},
|
|
939
|
-
pid: status.pid,
|
|
940
|
-
isRunning: status.isRunning,
|
|
941
|
-
availableModels: runningServers.map(s => s.modelName),
|
|
942
|
-
createdAt: config.createdAt,
|
|
943
|
-
lastStarted: config.lastStarted,
|
|
944
|
-
lastStopped: config.lastStopped,
|
|
945
|
-
});
|
|
946
|
-
} catch (error) {
|
|
947
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_STATUS_ERROR');
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Start router service
|
|
953
|
-
*/
|
|
954
|
-
private async handleStartRouter(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
955
|
-
try {
|
|
956
|
-
await routerManager.start();
|
|
957
|
-
|
|
958
|
-
const routerStatus = await routerManager.getStatus();
|
|
959
|
-
this.sendJson(res, 200, {
|
|
960
|
-
success: true,
|
|
961
|
-
status: 'running',
|
|
962
|
-
pid: routerStatus?.status.pid,
|
|
963
|
-
});
|
|
964
|
-
} catch (error) {
|
|
965
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_START_ERROR');
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Stop router service
|
|
971
|
-
*/
|
|
972
|
-
private async handleStopRouter(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
973
|
-
try {
|
|
974
|
-
await routerManager.stop();
|
|
975
|
-
|
|
976
|
-
this.sendJson(res, 200, {
|
|
977
|
-
success: true,
|
|
978
|
-
status: 'stopped',
|
|
979
|
-
});
|
|
980
|
-
} catch (error) {
|
|
981
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_STOP_ERROR');
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Restart router service
|
|
987
|
-
*/
|
|
988
|
-
private async handleRestartRouter(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
989
|
-
try {
|
|
990
|
-
await routerManager.restart();
|
|
991
|
-
|
|
992
|
-
const routerStatus = await routerManager.getStatus();
|
|
993
|
-
this.sendJson(res, 200, {
|
|
994
|
-
success: true,
|
|
995
|
-
status: 'running',
|
|
996
|
-
pid: routerStatus?.status.pid,
|
|
997
|
-
});
|
|
998
|
-
} catch (error) {
|
|
999
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_RESTART_ERROR');
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* Get router logs
|
|
1005
|
-
*/
|
|
1006
|
-
private async handleGetRouterLogs(req: http.IncomingMessage, res: http.ServerResponse, url: URL): Promise<void> {
|
|
1007
|
-
try {
|
|
1008
|
-
const config = await routerManager.loadConfig();
|
|
1009
|
-
if (!config) {
|
|
1010
|
-
this.sendError(res, 404, 'Not Found', 'Router not configured', 'ROUTER_NOT_FOUND');
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
const type = url.searchParams.get('type') || 'both'; // stdout, stderr, or both
|
|
1015
|
-
const lines = parseInt(url.searchParams.get('lines') || '100');
|
|
1016
|
-
|
|
1017
|
-
let stdout = '';
|
|
1018
|
-
let stderr = '';
|
|
1019
|
-
|
|
1020
|
-
if ((type === 'stdout' || type === 'both') && (await fileExists(config.stdoutPath))) {
|
|
1021
|
-
const content = await fs.readFile(config.stdoutPath, 'utf-8');
|
|
1022
|
-
const logLines = content.split('\n');
|
|
1023
|
-
stdout = logLines.slice(-lines).join('\n');
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
if ((type === 'stderr' || type === 'both') && (await fileExists(config.stderrPath))) {
|
|
1027
|
-
const content = await fs.readFile(config.stderrPath, 'utf-8');
|
|
1028
|
-
const logLines = content.split('\n');
|
|
1029
|
-
stderr = logLines.slice(-lines).join('\n');
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
this.sendJson(res, 200, { stdout, stderr });
|
|
1033
|
-
} catch (error) {
|
|
1034
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_LOGS_ERROR');
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
/**
|
|
1039
|
-
* Update router configuration
|
|
1040
|
-
*/
|
|
1041
|
-
private async handleUpdateRouter(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
1042
|
-
try {
|
|
1043
|
-
const body = await this.readBody(req);
|
|
1044
|
-
const updates = JSON.parse(body);
|
|
1045
|
-
|
|
1046
|
-
const config = await routerManager.loadConfig();
|
|
1047
|
-
if (!config) {
|
|
1048
|
-
this.sendError(res, 404, 'Not Found', 'Router not configured', 'ROUTER_NOT_FOUND');
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Validate updates
|
|
1053
|
-
const allowedFields = ['port', 'host', 'verbose', 'requestTimeout', 'healthCheckInterval'];
|
|
1054
|
-
const invalidFields = Object.keys(updates).filter(key => !allowedFields.includes(key));
|
|
1055
|
-
|
|
1056
|
-
if (invalidFields.length > 0) {
|
|
1057
|
-
this.sendError(res, 400, 'Bad Request', `Invalid fields: ${invalidFields.join(', ')}`, 'INVALID_FIELDS');
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Apply updates
|
|
1062
|
-
const needsRestart = updates.port !== undefined || updates.host !== undefined;
|
|
1063
|
-
await routerManager.updateConfig(updates);
|
|
1064
|
-
|
|
1065
|
-
// Regenerate plist if needed
|
|
1066
|
-
if (needsRestart) {
|
|
1067
|
-
const updatedConfig = await routerManager.loadConfig();
|
|
1068
|
-
if (updatedConfig) {
|
|
1069
|
-
await routerManager.createPlist(updatedConfig);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
this.sendJson(res, 200, {
|
|
1074
|
-
success: true,
|
|
1075
|
-
needsRestart,
|
|
1076
|
-
config: await routerManager.loadConfig(),
|
|
1077
|
-
});
|
|
1078
|
-
} catch (error) {
|
|
1079
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'ROUTER_UPDATE_ERROR');
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/**
|
|
1084
|
-
* Authenticate request via API key
|
|
1085
|
-
*/
|
|
1086
|
-
private authenticate(req: http.IncomingMessage): boolean {
|
|
1087
|
-
const authHeader = req.headers['authorization'];
|
|
1088
|
-
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
1089
|
-
const queryKey = url.searchParams.get('api_key');
|
|
1090
|
-
|
|
1091
|
-
const providedKey = authHeader?.replace('Bearer ', '') || queryKey;
|
|
1092
|
-
return providedKey === this.config.apiKey;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
* Read request body as string
|
|
1097
|
-
*/
|
|
1098
|
-
private async readBody(req: http.IncomingMessage): Promise<string> {
|
|
1099
|
-
return new Promise((resolve, reject) => {
|
|
1100
|
-
const chunks: Buffer[] = [];
|
|
1101
|
-
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
1102
|
-
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
1103
|
-
req.on('error', reject);
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
/**
|
|
1108
|
-
* Send JSON response
|
|
1109
|
-
*/
|
|
1110
|
-
private sendJson(res: http.ServerResponse, statusCode: number, data: any): void {
|
|
1111
|
-
if (res.headersSent) return;
|
|
1112
|
-
|
|
1113
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1114
|
-
res.end(JSON.stringify(data, null, 2));
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
/**
|
|
1118
|
-
* Send error response
|
|
1119
|
-
*/
|
|
1120
|
-
private sendError(res: http.ServerResponse, statusCode: number, error: string, details?: string, code?: string): void {
|
|
1121
|
-
if (res.headersSent) return;
|
|
1122
|
-
|
|
1123
|
-
const response: ErrorResponse = { error };
|
|
1124
|
-
if (details) response.details = details;
|
|
1125
|
-
if (code) response.code = code;
|
|
1126
|
-
|
|
1127
|
-
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
1128
|
-
res.end(JSON.stringify(response, null, 2));
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
/**
|
|
1132
|
-
* Serve static files from web/dist directory
|
|
1133
|
-
*/
|
|
1134
|
-
private async handleStaticFile(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
|
|
1135
|
-
try {
|
|
1136
|
-
// Resolve web/dist directory relative to project root
|
|
1137
|
-
const projectRoot = path.resolve(__dirname, '../..');
|
|
1138
|
-
const distDir = path.join(projectRoot, 'web', 'dist');
|
|
1139
|
-
|
|
1140
|
-
// Determine file path (default to index.html for SPA routing)
|
|
1141
|
-
let filePath: string;
|
|
1142
|
-
if (pathname === '/' || !path.extname(pathname)) {
|
|
1143
|
-
filePath = path.join(distDir, 'index.html');
|
|
1144
|
-
} else {
|
|
1145
|
-
filePath = path.join(distDir, pathname);
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// Security: Ensure file is within dist directory
|
|
1149
|
-
const resolvedPath = path.resolve(filePath);
|
|
1150
|
-
if (!resolvedPath.startsWith(distDir)) {
|
|
1151
|
-
this.sendError(res, 403, 'Forbidden', 'Access denied', 'FORBIDDEN');
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// Check if file exists
|
|
1156
|
-
if (!(await fileExists(resolvedPath))) {
|
|
1157
|
-
// For SPA routing, serve index.html for non-existent routes
|
|
1158
|
-
if (pathname !== '/' && !path.extname(pathname)) {
|
|
1159
|
-
const indexPath = path.join(distDir, 'index.html');
|
|
1160
|
-
if (await fileExists(indexPath)) {
|
|
1161
|
-
filePath = indexPath;
|
|
1162
|
-
} else {
|
|
1163
|
-
this.sendError(res, 404, 'Not Found', 'Static files not built. Run: cd web && npm install && npm run build', 'STATIC_NOT_FOUND');
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
} else {
|
|
1167
|
-
this.sendError(res, 404, 'Not Found', 'Static files not built. Run: cd web && npm install && npm run build', 'STATIC_NOT_FOUND');
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
} else {
|
|
1171
|
-
filePath = resolvedPath;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// Determine content type
|
|
1175
|
-
const ext = path.extname(filePath);
|
|
1176
|
-
const contentTypes: Record<string, string> = {
|
|
1177
|
-
'.html': 'text/html; charset=utf-8',
|
|
1178
|
-
'.js': 'application/javascript; charset=utf-8',
|
|
1179
|
-
'.css': 'text/css; charset=utf-8',
|
|
1180
|
-
'.json': 'application/json; charset=utf-8',
|
|
1181
|
-
'.png': 'image/png',
|
|
1182
|
-
'.jpg': 'image/jpeg',
|
|
1183
|
-
'.jpeg': 'image/jpeg',
|
|
1184
|
-
'.gif': 'image/gif',
|
|
1185
|
-
'.svg': 'image/svg+xml',
|
|
1186
|
-
'.ico': 'image/x-icon',
|
|
1187
|
-
'.woff': 'font/woff',
|
|
1188
|
-
'.woff2': 'font/woff2',
|
|
1189
|
-
'.ttf': 'font/ttf',
|
|
1190
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
1191
|
-
};
|
|
1192
|
-
const contentType = contentTypes[ext] || 'application/octet-stream';
|
|
1193
|
-
|
|
1194
|
-
// Read and serve file
|
|
1195
|
-
const content = await fs.readFile(filePath);
|
|
1196
|
-
res.writeHead(200, {
|
|
1197
|
-
'Content-Type': contentType,
|
|
1198
|
-
'Content-Length': content.length,
|
|
1199
|
-
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=31536000',
|
|
1200
|
-
});
|
|
1201
|
-
res.end(content);
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
console.error('[Admin] Error serving static file:', error);
|
|
1204
|
-
this.sendError(res, 500, 'Internal Server Error', (error as Error).message, 'STATIC_ERROR');
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
/**
|
|
1209
|
-
* Log request
|
|
1210
|
-
*/
|
|
1211
|
-
private logRequest(method: string, pathname: string): void {
|
|
1212
|
-
if (this.config.verbose) {
|
|
1213
|
-
console.log(`[Admin] ${method} ${pathname}`);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
/**
|
|
1218
|
-
* Log response
|
|
1219
|
-
*/
|
|
1220
|
-
private logResponse(method: string, pathname: string, statusCode: number, durationMs: number): void {
|
|
1221
|
-
if (this.config.verbose) {
|
|
1222
|
-
console.log(`[Admin] ${method} ${pathname} ${statusCode} ${durationMs}ms`);
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// Main entry point
|
|
1228
|
-
async function main() {
|
|
1229
|
-
try {
|
|
1230
|
-
const server = new AdminServer();
|
|
1231
|
-
await server.start();
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
console.error('[Admin] Failed to start:', error);
|
|
1234
|
-
process.exit(1);
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Only run if this is the main module
|
|
1239
|
-
if (require.main === module) {
|
|
1240
|
-
main();
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
export { AdminServer };
|