@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.
- package/CHANGELOG.md +10 -0
- package/README.md +356 -3
- package/dist/cli.js +99 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin/config.d.ts +10 -0
- package/dist/commands/admin/config.d.ts.map +1 -0
- package/dist/commands/admin/config.js +100 -0
- package/dist/commands/admin/config.js.map +1 -0
- package/dist/commands/admin/logs.d.ts +10 -0
- package/dist/commands/admin/logs.d.ts.map +1 -0
- package/dist/commands/admin/logs.js +114 -0
- package/dist/commands/admin/logs.js.map +1 -0
- package/dist/commands/admin/restart.d.ts +2 -0
- package/dist/commands/admin/restart.d.ts.map +1 -0
- package/dist/commands/admin/restart.js +29 -0
- package/dist/commands/admin/restart.js.map +1 -0
- package/dist/commands/admin/start.d.ts +2 -0
- package/dist/commands/admin/start.d.ts.map +1 -0
- package/dist/commands/admin/start.js +30 -0
- package/dist/commands/admin/start.js.map +1 -0
- package/dist/commands/admin/status.d.ts +2 -0
- package/dist/commands/admin/status.d.ts.map +1 -0
- package/dist/commands/admin/status.js +82 -0
- package/dist/commands/admin/status.js.map +1 -0
- package/dist/commands/admin/stop.d.ts +2 -0
- package/dist/commands/admin/stop.d.ts.map +1 -0
- package/dist/commands/admin/stop.js +21 -0
- package/dist/commands/admin/stop.js.map +1 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +22 -0
- package/dist/commands/logs.js.map +1 -1
- package/dist/lib/admin-manager.d.ts +111 -0
- package/dist/lib/admin-manager.d.ts.map +1 -0
- package/dist/lib/admin-manager.js +413 -0
- package/dist/lib/admin-manager.js.map +1 -0
- package/dist/lib/admin-server.d.ts +148 -0
- package/dist/lib/admin-server.d.ts.map +1 -0
- package/dist/lib/admin-server.js +1161 -0
- package/dist/lib/admin-server.js.map +1 -0
- package/dist/lib/download-job-manager.d.ts +64 -0
- package/dist/lib/download-job-manager.d.ts.map +1 -0
- package/dist/lib/download-job-manager.js +164 -0
- package/dist/lib/download-job-manager.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.js +1 -1
- package/dist/types/admin-config.d.ts +19 -0
- package/dist/types/admin-config.d.ts.map +1 -0
- package/dist/types/admin-config.js +3 -0
- package/dist/types/admin-config.js.map +1 -0
- package/dist/utils/log-parser.d.ts +9 -0
- package/dist/utils/log-parser.d.ts.map +1 -1
- package/dist/utils/log-parser.js +11 -0
- package/dist/utils/log-parser.js.map +1 -1
- package/docs/images/web-ui-servers.png +0 -0
- package/package.json +1 -1
- package/src/cli.ts +100 -0
- package/src/commands/admin/config.ts +121 -0
- package/src/commands/admin/logs.ts +91 -0
- package/src/commands/admin/restart.ts +26 -0
- package/src/commands/admin/start.ts +27 -0
- package/src/commands/admin/status.ts +84 -0
- package/src/commands/admin/stop.ts +16 -0
- package/src/commands/logs.ts +24 -0
- package/src/lib/admin-manager.ts +435 -0
- package/src/lib/admin-server.ts +1243 -0
- package/src/lib/download-job-manager.ts +213 -0
- package/src/tui/MultiServerMonitorApp.ts +1 -1
- package/src/types/admin-config.ts +25 -0
- package/src/utils/log-parser.ts +13 -0
- package/web/README.md +429 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +13 -0
- package/web/llamacpp-web-dist.tar.gz +0 -0
- package/web/package-lock.json +4017 -0
- package/web/package.json +38 -0
- package/web/postcss.config.js +6 -0
- package/web/public/vite.svg +1 -0
- package/web/src/App.css +42 -0
- package/web/src/App.tsx +86 -0
- package/web/src/assets/react.svg +1 -0
- package/web/src/components/ApiKeyPrompt.tsx +71 -0
- package/web/src/components/CreateServerModal.tsx +372 -0
- package/web/src/components/DownloadProgress.tsx +123 -0
- package/web/src/components/Nav.tsx +89 -0
- package/web/src/components/RouterConfigModal.tsx +240 -0
- package/web/src/components/SearchModal.tsx +306 -0
- package/web/src/components/ServerConfigModal.tsx +291 -0
- package/web/src/hooks/useApi.ts +259 -0
- package/web/src/index.css +42 -0
- package/web/src/lib/api.ts +226 -0
- package/web/src/main.tsx +10 -0
- package/web/src/pages/Dashboard.tsx +103 -0
- package/web/src/pages/Models.tsx +258 -0
- package/web/src/pages/Router.tsx +270 -0
- package/web/src/pages/RouterLogs.tsx +201 -0
- package/web/src/pages/ServerLogs.tsx +553 -0
- package/web/src/pages/Servers.tsx +358 -0
- package/web/src/types/api.ts +140 -0
- package/web/tailwind.config.js +31 -0
- package/web/tsconfig.app.json +28 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +26 -0
- package/web/vite.config.ts +25 -0
- package/MONITORING-ACCURACY-FIX.md +0 -199
- 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
|