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