@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.
Files changed (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -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 };