@appkit/llamacpp-cli 1.8.0 → 1.10.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 +58 -0
- package/README.md +249 -40
- package/dist/cli.js +154 -10
- package/dist/cli.js.map +1 -1
- package/dist/commands/completion.d.ts +9 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +83 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/monitor.js +1 -1
- package/dist/commands/monitor.js.map +1 -1
- package/dist/commands/ps.d.ts +1 -3
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +36 -115
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/router/config.d.ts +11 -0
- package/dist/commands/router/config.d.ts.map +1 -0
- package/dist/commands/router/config.js +100 -0
- package/dist/commands/router/config.js.map +1 -0
- package/dist/commands/router/logs.d.ts +12 -0
- package/dist/commands/router/logs.d.ts.map +1 -0
- package/dist/commands/router/logs.js +238 -0
- package/dist/commands/router/logs.js.map +1 -0
- package/dist/commands/router/restart.d.ts +2 -0
- package/dist/commands/router/restart.d.ts.map +1 -0
- package/dist/commands/router/restart.js +39 -0
- package/dist/commands/router/restart.js.map +1 -0
- package/dist/commands/router/start.d.ts +2 -0
- package/dist/commands/router/start.d.ts.map +1 -0
- package/dist/commands/router/start.js +60 -0
- package/dist/commands/router/start.js.map +1 -0
- package/dist/commands/router/status.d.ts +2 -0
- package/dist/commands/router/status.d.ts.map +1 -0
- package/dist/commands/router/status.js +116 -0
- package/dist/commands/router/status.js.map +1 -0
- package/dist/commands/router/stop.d.ts +2 -0
- package/dist/commands/router/stop.d.ts.map +1 -0
- package/dist/commands/router/stop.js +36 -0
- package/dist/commands/router/stop.js.map +1 -0
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +27 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/completion.d.ts +5 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +195 -0
- package/dist/lib/completion.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +5 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +53 -20
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/router-logger.d.ts +61 -0
- package/dist/lib/router-logger.d.ts.map +1 -0
- package/dist/lib/router-logger.js +200 -0
- package/dist/lib/router-logger.js.map +1 -0
- package/dist/lib/router-manager.d.ts +103 -0
- package/dist/lib/router-manager.d.ts.map +1 -0
- package/dist/lib/router-manager.js +394 -0
- package/dist/lib/router-manager.js.map +1 -0
- package/dist/lib/router-server.d.ts +61 -0
- package/dist/lib/router-server.d.ts.map +1 -0
- package/dist/lib/router-server.js +485 -0
- package/dist/lib/router-server.js.map +1 -0
- package/dist/tui/ConfigApp.d.ts +7 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -0
- package/dist/tui/ConfigApp.js +1002 -0
- package/dist/tui/ConfigApp.js.map +1 -0
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +85 -49
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts +7 -0
- package/dist/tui/ModelsApp.d.ts.map +1 -0
- package/dist/tui/ModelsApp.js +362 -0
- package/dist/tui/ModelsApp.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +1038 -122
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts +7 -0
- package/dist/tui/RootNavigator.d.ts.map +1 -0
- package/dist/tui/RootNavigator.js +55 -0
- package/dist/tui/RootNavigator.js.map +1 -0
- package/dist/tui/SearchApp.d.ts +6 -0
- package/dist/tui/SearchApp.d.ts.map +1 -0
- package/dist/tui/SearchApp.js +451 -0
- package/dist/tui/SearchApp.js.map +1 -0
- package/dist/tui/SplashScreen.d.ts +16 -0
- package/dist/tui/SplashScreen.d.ts.map +1 -0
- package/dist/tui/SplashScreen.js +129 -0
- package/dist/tui/SplashScreen.js.map +1 -0
- package/dist/types/router-config.d.ts +19 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +3 -0
- package/dist/types/router-config.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +121 -10
- package/src/commands/monitor.ts +1 -1
- package/src/commands/ps.ts +44 -133
- package/src/commands/router/config.ts +116 -0
- package/src/commands/router/logs.ts +256 -0
- package/src/commands/router/restart.ts +36 -0
- package/src/commands/router/start.ts +60 -0
- package/src/commands/router/status.ts +119 -0
- package/src/commands/router/stop.ts +33 -0
- package/src/commands/tui.ts +25 -0
- package/src/lib/model-downloader.ts +57 -20
- package/src/lib/router-logger.ts +201 -0
- package/src/lib/router-manager.ts +414 -0
- package/src/lib/router-server.ts +538 -0
- package/src/tui/ConfigApp.ts +1085 -0
- package/src/tui/HistoricalMonitorApp.ts +88 -49
- package/src/tui/ModelsApp.ts +368 -0
- package/src/tui/MultiServerMonitorApp.ts +1163 -122
- package/src/tui/RootNavigator.ts +74 -0
- package/src/tui/SearchApp.ts +511 -0
- package/src/tui/SplashScreen.ts +149 -0
- package/src/types/router-config.ts +25 -0
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createConfigUI = createConfigUI;
|
|
40
|
+
const blessed_1 = __importDefault(require("blessed"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const server_config_js_1 = require("../types/server-config.js");
|
|
43
|
+
const state_manager_js_1 = require("../lib/state-manager.js");
|
|
44
|
+
const launchctl_manager_js_1 = require("../lib/launchctl-manager.js");
|
|
45
|
+
const status_checker_js_1 = require("../lib/status-checker.js");
|
|
46
|
+
const port_manager_js_1 = require("../lib/port-manager.js");
|
|
47
|
+
const model_scanner_js_1 = require("../lib/model-scanner.js");
|
|
48
|
+
const file_utils_js_1 = require("../utils/file-utils.js");
|
|
49
|
+
const log_utils_js_1 = require("../utils/log-utils.js");
|
|
50
|
+
/**
|
|
51
|
+
* Config screen TUI for editing server configuration
|
|
52
|
+
*/
|
|
53
|
+
async function createConfigUI(screen, server, onBack) {
|
|
54
|
+
// Initialize state
|
|
55
|
+
const state = {
|
|
56
|
+
fields: [
|
|
57
|
+
{
|
|
58
|
+
key: 'model',
|
|
59
|
+
label: 'Model',
|
|
60
|
+
type: 'model',
|
|
61
|
+
value: server.modelName,
|
|
62
|
+
originalValue: server.modelName,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: 'host',
|
|
66
|
+
label: 'Host',
|
|
67
|
+
type: 'select',
|
|
68
|
+
value: server.host || '127.0.0.1',
|
|
69
|
+
originalValue: server.host || '127.0.0.1',
|
|
70
|
+
options: ['127.0.0.1', '0.0.0.0'],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
key: 'port',
|
|
74
|
+
label: 'Port',
|
|
75
|
+
type: 'number',
|
|
76
|
+
value: server.port,
|
|
77
|
+
originalValue: server.port,
|
|
78
|
+
validation: (value) => {
|
|
79
|
+
if (value < 1024)
|
|
80
|
+
return 'Port must be >= 1024';
|
|
81
|
+
if (value > 65535)
|
|
82
|
+
return 'Port must be <= 65535';
|
|
83
|
+
return null;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'threads',
|
|
88
|
+
label: 'Threads',
|
|
89
|
+
type: 'number',
|
|
90
|
+
value: server.threads,
|
|
91
|
+
originalValue: server.threads,
|
|
92
|
+
validation: (value) => {
|
|
93
|
+
if (value < 1)
|
|
94
|
+
return 'Must be at least 1';
|
|
95
|
+
if (value > 256)
|
|
96
|
+
return 'Must be at most 256';
|
|
97
|
+
return null;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: 'ctxSize',
|
|
102
|
+
label: 'Context Size',
|
|
103
|
+
type: 'number',
|
|
104
|
+
value: server.ctxSize,
|
|
105
|
+
originalValue: server.ctxSize,
|
|
106
|
+
validation: (value) => {
|
|
107
|
+
if (value < 512)
|
|
108
|
+
return 'Must be at least 512';
|
|
109
|
+
if (value > 131072)
|
|
110
|
+
return 'Must be at most 131072';
|
|
111
|
+
return null;
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: 'gpuLayers',
|
|
116
|
+
label: 'GPU Layers',
|
|
117
|
+
type: 'number',
|
|
118
|
+
value: server.gpuLayers,
|
|
119
|
+
originalValue: server.gpuLayers,
|
|
120
|
+
validation: (value) => {
|
|
121
|
+
if (value < 0)
|
|
122
|
+
return 'Must be at least 0';
|
|
123
|
+
if (value > 999)
|
|
124
|
+
return 'Must be at most 999';
|
|
125
|
+
return null;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: 'verbose',
|
|
130
|
+
label: 'Verbose Logs',
|
|
131
|
+
type: 'toggle',
|
|
132
|
+
value: server.verbose,
|
|
133
|
+
originalValue: server.verbose,
|
|
134
|
+
options: ['Disabled', 'Enabled'],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
key: 'customFlags',
|
|
138
|
+
label: 'Custom Flags',
|
|
139
|
+
type: 'text',
|
|
140
|
+
value: server.customFlags?.join(', ') || '',
|
|
141
|
+
originalValue: server.customFlags?.join(', ') || '',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
selectedIndex: 0,
|
|
145
|
+
hasChanges: false,
|
|
146
|
+
};
|
|
147
|
+
// Create content box
|
|
148
|
+
const contentBox = blessed_1.default.box({
|
|
149
|
+
top: 0,
|
|
150
|
+
left: 0,
|
|
151
|
+
width: '100%',
|
|
152
|
+
height: '100%',
|
|
153
|
+
tags: true,
|
|
154
|
+
scrollable: true,
|
|
155
|
+
alwaysScroll: true,
|
|
156
|
+
keys: true,
|
|
157
|
+
vi: true,
|
|
158
|
+
mouse: true,
|
|
159
|
+
scrollbar: {
|
|
160
|
+
ch: '█',
|
|
161
|
+
style: { fg: 'blue' },
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
screen.append(contentBox);
|
|
165
|
+
// Check if any field has changed
|
|
166
|
+
function updateHasChanges() {
|
|
167
|
+
state.hasChanges = state.fields.some(f => {
|
|
168
|
+
if (typeof f.value === 'string' && typeof f.originalValue === 'string') {
|
|
169
|
+
return f.value.trim() !== f.originalValue.trim();
|
|
170
|
+
}
|
|
171
|
+
return f.value !== f.originalValue;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Parse context size with k/K suffix support (e.g., "4k" -> 4096, "64K" -> 65536)
|
|
175
|
+
function parseContextSize(input) {
|
|
176
|
+
const trimmed = input.trim().toLowerCase();
|
|
177
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(k)?$/);
|
|
178
|
+
if (!match)
|
|
179
|
+
return null;
|
|
180
|
+
const num = parseFloat(match[1]);
|
|
181
|
+
const hasK = match[2] === 'k';
|
|
182
|
+
if (isNaN(num) || num <= 0)
|
|
183
|
+
return null;
|
|
184
|
+
return hasK ? Math.round(num * 1024) : Math.round(num);
|
|
185
|
+
}
|
|
186
|
+
// Format context size for display (e.g., 4096 -> "4k", 65536 -> "64k")
|
|
187
|
+
function formatContextSize(value) {
|
|
188
|
+
if (value >= 1024 && value % 1024 === 0) {
|
|
189
|
+
return `${value / 1024}k`;
|
|
190
|
+
}
|
|
191
|
+
return value.toLocaleString();
|
|
192
|
+
}
|
|
193
|
+
// Format display value for a field
|
|
194
|
+
function formatValue(field) {
|
|
195
|
+
if (field.type === 'toggle') {
|
|
196
|
+
return field.value ? 'Enabled' : 'Disabled';
|
|
197
|
+
}
|
|
198
|
+
if (field.type === 'text' && !field.value) {
|
|
199
|
+
return '(none)';
|
|
200
|
+
}
|
|
201
|
+
if (field.type === 'number') {
|
|
202
|
+
// Don't use commas for port numbers
|
|
203
|
+
if (field.key === 'port') {
|
|
204
|
+
return String(field.value);
|
|
205
|
+
}
|
|
206
|
+
// Use k notation for context size
|
|
207
|
+
if (field.key === 'ctxSize') {
|
|
208
|
+
return formatContextSize(field.value);
|
|
209
|
+
}
|
|
210
|
+
return field.value.toLocaleString();
|
|
211
|
+
}
|
|
212
|
+
return String(field.value);
|
|
213
|
+
}
|
|
214
|
+
// Render the main config screen
|
|
215
|
+
function render() {
|
|
216
|
+
const termWidth = screen.width || 80;
|
|
217
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
218
|
+
let content = '';
|
|
219
|
+
// Header
|
|
220
|
+
content += `{bold}{blue-fg}═══ Configure: ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
|
|
221
|
+
// Section header
|
|
222
|
+
content += '{bold}Server Configuration{/bold}\n';
|
|
223
|
+
content += divider + '\n';
|
|
224
|
+
// Fields
|
|
225
|
+
for (let i = 0; i < state.fields.length; i++) {
|
|
226
|
+
const field = state.fields[i];
|
|
227
|
+
const isSelected = i === state.selectedIndex;
|
|
228
|
+
const hasChanged = field.value !== field.originalValue;
|
|
229
|
+
const indicator = isSelected ? '►' : ' ';
|
|
230
|
+
const label = field.label.padEnd(16);
|
|
231
|
+
const value = formatValue(field);
|
|
232
|
+
// Color coding: cyan for selected, yellow for changed
|
|
233
|
+
let valueDisplay = value;
|
|
234
|
+
if (hasChanged) {
|
|
235
|
+
valueDisplay = `{yellow-fg}${value}{/yellow-fg}`;
|
|
236
|
+
}
|
|
237
|
+
if (isSelected) {
|
|
238
|
+
content += `{cyan-bg}{15-fg} ${indicator} ${label}${value}{/15-fg}{/cyan-bg}\n`;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
content += ` ${indicator} ${label}${valueDisplay}\n`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
content += divider + '\n\n';
|
|
245
|
+
// Show changes summary if any
|
|
246
|
+
if (state.hasChanges) {
|
|
247
|
+
content += '{yellow-fg}* Unsaved Changes:{/yellow-fg}\n';
|
|
248
|
+
for (const field of state.fields) {
|
|
249
|
+
if (field.value !== field.originalValue) {
|
|
250
|
+
const oldVal = field.type === 'toggle'
|
|
251
|
+
? (field.originalValue ? 'Enabled' : 'Disabled')
|
|
252
|
+
: (field.originalValue || '(none)');
|
|
253
|
+
const newVal = field.type === 'toggle'
|
|
254
|
+
? (field.value ? 'Enabled' : 'Disabled')
|
|
255
|
+
: (field.value || '(none)');
|
|
256
|
+
content += ` ${field.label}: ${oldVal} → {yellow-fg}${newVal}{/yellow-fg}\n`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
content += '\n';
|
|
260
|
+
}
|
|
261
|
+
// Footer
|
|
262
|
+
content += divider + '\n';
|
|
263
|
+
content += '{gray-fg}[↑/↓] Navigate [Enter] Edit [S]ave [ESC] Cancel{/gray-fg}';
|
|
264
|
+
contentBox.setContent(content);
|
|
265
|
+
screen.render();
|
|
266
|
+
}
|
|
267
|
+
// Create a centered modal box
|
|
268
|
+
function createModal(title, height = 'shrink') {
|
|
269
|
+
return blessed_1.default.box({
|
|
270
|
+
parent: screen,
|
|
271
|
+
top: 'center',
|
|
272
|
+
left: 'center',
|
|
273
|
+
width: '60%',
|
|
274
|
+
height,
|
|
275
|
+
border: { type: 'line' },
|
|
276
|
+
style: {
|
|
277
|
+
border: { fg: 'cyan' },
|
|
278
|
+
fg: 'white',
|
|
279
|
+
},
|
|
280
|
+
tags: true,
|
|
281
|
+
label: ` ${title} `,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// Number input modal
|
|
285
|
+
async function editNumber(field) {
|
|
286
|
+
unregisterHandlers();
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
const isCtxSize = field.key === 'ctxSize';
|
|
289
|
+
const modal = createModal(`Edit ${field.label}`, isCtxSize ? 11 : 10);
|
|
290
|
+
const currentDisplay = isCtxSize
|
|
291
|
+
? formatContextSize(field.value)
|
|
292
|
+
: field.value;
|
|
293
|
+
const infoText = blessed_1.default.text({
|
|
294
|
+
parent: modal,
|
|
295
|
+
top: 1,
|
|
296
|
+
left: 2,
|
|
297
|
+
content: `Current: ${currentDisplay}`,
|
|
298
|
+
tags: true,
|
|
299
|
+
});
|
|
300
|
+
// Add hint for context size
|
|
301
|
+
if (isCtxSize) {
|
|
302
|
+
blessed_1.default.text({
|
|
303
|
+
parent: modal,
|
|
304
|
+
top: 2,
|
|
305
|
+
left: 2,
|
|
306
|
+
content: '{gray-fg}Accepts: 4096, 4k, 8k, 16k, 32k, 64k, 128k{/gray-fg}',
|
|
307
|
+
tags: true,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const inputBox = blessed_1.default.textbox({
|
|
311
|
+
parent: modal,
|
|
312
|
+
top: isCtxSize ? 4 : 3,
|
|
313
|
+
left: 2,
|
|
314
|
+
right: 2,
|
|
315
|
+
height: 3,
|
|
316
|
+
inputOnFocus: true,
|
|
317
|
+
border: { type: 'line' },
|
|
318
|
+
style: {
|
|
319
|
+
border: { fg: 'white' },
|
|
320
|
+
focus: { border: { fg: 'green' } },
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
blessed_1.default.text({
|
|
324
|
+
parent: modal,
|
|
325
|
+
bottom: 1,
|
|
326
|
+
left: 2,
|
|
327
|
+
content: '{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}',
|
|
328
|
+
tags: true,
|
|
329
|
+
});
|
|
330
|
+
// Pre-fill with k notation for context size
|
|
331
|
+
const initialValue = isCtxSize
|
|
332
|
+
? formatContextSize(field.value)
|
|
333
|
+
: String(field.value);
|
|
334
|
+
inputBox.setValue(initialValue);
|
|
335
|
+
screen.render();
|
|
336
|
+
inputBox.focus();
|
|
337
|
+
inputBox.on('submit', (value) => {
|
|
338
|
+
let numValue;
|
|
339
|
+
if (isCtxSize) {
|
|
340
|
+
numValue = parseContextSize(value);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
numValue = parseInt(value, 10);
|
|
344
|
+
if (isNaN(numValue))
|
|
345
|
+
numValue = null;
|
|
346
|
+
}
|
|
347
|
+
if (numValue !== null) {
|
|
348
|
+
if (field.validation) {
|
|
349
|
+
const error = field.validation(numValue);
|
|
350
|
+
if (error) {
|
|
351
|
+
infoText.setContent(`{red-fg}Error: ${error}{/red-fg}`);
|
|
352
|
+
screen.render();
|
|
353
|
+
inputBox.focus();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
field.value = numValue;
|
|
358
|
+
updateHasChanges();
|
|
359
|
+
}
|
|
360
|
+
screen.remove(modal);
|
|
361
|
+
registerHandlers();
|
|
362
|
+
render();
|
|
363
|
+
resolve();
|
|
364
|
+
});
|
|
365
|
+
inputBox.on('cancel', () => {
|
|
366
|
+
screen.remove(modal);
|
|
367
|
+
registerHandlers();
|
|
368
|
+
render();
|
|
369
|
+
resolve();
|
|
370
|
+
});
|
|
371
|
+
inputBox.key(['escape'], () => {
|
|
372
|
+
screen.remove(modal);
|
|
373
|
+
registerHandlers();
|
|
374
|
+
render();
|
|
375
|
+
resolve();
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Toggle/select modal
|
|
380
|
+
async function editSelect(field) {
|
|
381
|
+
unregisterHandlers();
|
|
382
|
+
return new Promise((resolve) => {
|
|
383
|
+
const options = field.options || [];
|
|
384
|
+
let selectedOption = field.type === 'toggle'
|
|
385
|
+
? (field.value ? 1 : 0)
|
|
386
|
+
: options.indexOf(String(field.value));
|
|
387
|
+
if (selectedOption < 0)
|
|
388
|
+
selectedOption = 0;
|
|
389
|
+
const modal = createModal(field.label, options.length + 6);
|
|
390
|
+
function renderOptions() {
|
|
391
|
+
let content = '\n';
|
|
392
|
+
for (let i = 0; i < options.length; i++) {
|
|
393
|
+
const isSelected = i === selectedOption;
|
|
394
|
+
const indicator = isSelected ? '●' : '○';
|
|
395
|
+
if (isSelected) {
|
|
396
|
+
content += ` {cyan-fg}${indicator} ${options[i]}{/cyan-fg}\n`;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
content += ` {gray-fg}${indicator} ${options[i]}{/gray-fg}\n`;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Add warning for 0.0.0.0
|
|
403
|
+
if (field.key === 'host' && options[selectedOption] === '0.0.0.0') {
|
|
404
|
+
content += '\n {yellow-fg}⚠ Warning: Exposes server to network{/yellow-fg}';
|
|
405
|
+
}
|
|
406
|
+
content += '\n\n{gray-fg} [↑/↓] Select [Enter] Confirm [ESC] Cancel{/gray-fg}';
|
|
407
|
+
modal.setContent(content);
|
|
408
|
+
screen.render();
|
|
409
|
+
}
|
|
410
|
+
renderOptions();
|
|
411
|
+
modal.focus();
|
|
412
|
+
modal.key(['up', 'k'], () => {
|
|
413
|
+
selectedOption = Math.max(0, selectedOption - 1);
|
|
414
|
+
renderOptions();
|
|
415
|
+
});
|
|
416
|
+
modal.key(['down', 'j'], () => {
|
|
417
|
+
selectedOption = Math.min(options.length - 1, selectedOption + 1);
|
|
418
|
+
renderOptions();
|
|
419
|
+
});
|
|
420
|
+
modal.key(['enter'], () => {
|
|
421
|
+
if (field.type === 'toggle') {
|
|
422
|
+
field.value = selectedOption === 1;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
field.value = options[selectedOption];
|
|
426
|
+
}
|
|
427
|
+
updateHasChanges();
|
|
428
|
+
screen.remove(modal);
|
|
429
|
+
registerHandlers();
|
|
430
|
+
render();
|
|
431
|
+
resolve();
|
|
432
|
+
});
|
|
433
|
+
modal.key(['escape'], () => {
|
|
434
|
+
screen.remove(modal);
|
|
435
|
+
registerHandlers();
|
|
436
|
+
render();
|
|
437
|
+
resolve();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Text input modal
|
|
442
|
+
async function editText(field) {
|
|
443
|
+
unregisterHandlers();
|
|
444
|
+
return new Promise((resolve) => {
|
|
445
|
+
const modal = createModal(`Edit ${field.label}`, 10);
|
|
446
|
+
const infoText = blessed_1.default.text({
|
|
447
|
+
parent: modal,
|
|
448
|
+
top: 1,
|
|
449
|
+
left: 2,
|
|
450
|
+
content: 'Enter comma-separated flags:',
|
|
451
|
+
tags: true,
|
|
452
|
+
});
|
|
453
|
+
const inputBox = blessed_1.default.textbox({
|
|
454
|
+
parent: modal,
|
|
455
|
+
top: 3,
|
|
456
|
+
left: 2,
|
|
457
|
+
right: 2,
|
|
458
|
+
height: 3,
|
|
459
|
+
inputOnFocus: true,
|
|
460
|
+
border: { type: 'line' },
|
|
461
|
+
style: {
|
|
462
|
+
border: { fg: 'white' },
|
|
463
|
+
focus: { border: { fg: 'green' } },
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
const helpText = blessed_1.default.text({
|
|
467
|
+
parent: modal,
|
|
468
|
+
bottom: 1,
|
|
469
|
+
left: 2,
|
|
470
|
+
content: '{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}',
|
|
471
|
+
tags: true,
|
|
472
|
+
});
|
|
473
|
+
inputBox.setValue(field.value || '');
|
|
474
|
+
screen.render();
|
|
475
|
+
inputBox.focus();
|
|
476
|
+
inputBox.on('submit', (value) => {
|
|
477
|
+
field.value = value.trim();
|
|
478
|
+
updateHasChanges();
|
|
479
|
+
screen.remove(modal);
|
|
480
|
+
registerHandlers();
|
|
481
|
+
render();
|
|
482
|
+
resolve();
|
|
483
|
+
});
|
|
484
|
+
inputBox.on('cancel', () => {
|
|
485
|
+
screen.remove(modal);
|
|
486
|
+
registerHandlers();
|
|
487
|
+
render();
|
|
488
|
+
resolve();
|
|
489
|
+
});
|
|
490
|
+
inputBox.key(['escape'], () => {
|
|
491
|
+
screen.remove(modal);
|
|
492
|
+
registerHandlers();
|
|
493
|
+
render();
|
|
494
|
+
resolve();
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
// Model picker modal
|
|
499
|
+
async function editModel(field) {
|
|
500
|
+
unregisterHandlers();
|
|
501
|
+
return new Promise(async (resolve) => {
|
|
502
|
+
const models = await model_scanner_js_1.modelScanner.scanModels();
|
|
503
|
+
if (models.length === 0) {
|
|
504
|
+
// Show error modal
|
|
505
|
+
const errorModal = createModal('Error', 7);
|
|
506
|
+
errorModal.setContent('\n {red-fg}No models found in ~/models{/red-fg}\n\n {gray-fg}[ESC] Close{/gray-fg}');
|
|
507
|
+
screen.render();
|
|
508
|
+
errorModal.focus();
|
|
509
|
+
errorModal.key(['escape', 'enter'], () => {
|
|
510
|
+
screen.remove(errorModal);
|
|
511
|
+
registerHandlers();
|
|
512
|
+
render();
|
|
513
|
+
resolve();
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
let selectedIndex = models.findIndex(m => m.filename === field.value);
|
|
518
|
+
if (selectedIndex < 0)
|
|
519
|
+
selectedIndex = 0;
|
|
520
|
+
let scrollOffset = 0;
|
|
521
|
+
const maxVisible = 8;
|
|
522
|
+
const modal = createModal('Select Model', maxVisible + 6);
|
|
523
|
+
function renderModels() {
|
|
524
|
+
// Adjust scroll offset
|
|
525
|
+
if (selectedIndex < scrollOffset) {
|
|
526
|
+
scrollOffset = selectedIndex;
|
|
527
|
+
}
|
|
528
|
+
else if (selectedIndex >= scrollOffset + maxVisible) {
|
|
529
|
+
scrollOffset = selectedIndex - maxVisible + 1;
|
|
530
|
+
}
|
|
531
|
+
let content = '\n';
|
|
532
|
+
const visibleModels = models.slice(scrollOffset, scrollOffset + maxVisible);
|
|
533
|
+
for (let i = 0; i < visibleModels.length; i++) {
|
|
534
|
+
const model = visibleModels[i];
|
|
535
|
+
const actualIndex = scrollOffset + i;
|
|
536
|
+
const isSelected = actualIndex === selectedIndex;
|
|
537
|
+
const indicator = isSelected ? '►' : ' ';
|
|
538
|
+
// Truncate filename if too long
|
|
539
|
+
let displayName = model.filename;
|
|
540
|
+
const maxLen = 40;
|
|
541
|
+
if (displayName.length > maxLen) {
|
|
542
|
+
displayName = displayName.substring(0, maxLen - 3) + '...';
|
|
543
|
+
}
|
|
544
|
+
displayName = displayName.padEnd(maxLen);
|
|
545
|
+
const size = model.sizeFormatted.padStart(8);
|
|
546
|
+
if (isSelected) {
|
|
547
|
+
content += ` {cyan-bg}{15-fg}${indicator} ${displayName} ${size}{/15-fg}{/cyan-bg}\n`;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
content += ` ${indicator} ${displayName} {gray-fg}${size}{/gray-fg}\n`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Scroll indicator
|
|
554
|
+
if (models.length > maxVisible) {
|
|
555
|
+
const scrollInfo = `${selectedIndex + 1}/${models.length}`;
|
|
556
|
+
content += `\n {gray-fg}${scrollInfo}{/gray-fg}`;
|
|
557
|
+
}
|
|
558
|
+
content += '\n\n{gray-fg} [↑/↓] Navigate [Enter] Select [ESC] Cancel{/gray-fg}';
|
|
559
|
+
modal.setContent(content);
|
|
560
|
+
screen.render();
|
|
561
|
+
}
|
|
562
|
+
renderModels();
|
|
563
|
+
modal.focus();
|
|
564
|
+
modal.key(['up', 'k'], () => {
|
|
565
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
566
|
+
renderModels();
|
|
567
|
+
});
|
|
568
|
+
modal.key(['down', 'j'], () => {
|
|
569
|
+
selectedIndex = Math.min(models.length - 1, selectedIndex + 1);
|
|
570
|
+
renderModels();
|
|
571
|
+
});
|
|
572
|
+
modal.key(['enter'], () => {
|
|
573
|
+
field.value = models[selectedIndex].filename;
|
|
574
|
+
updateHasChanges();
|
|
575
|
+
screen.remove(modal);
|
|
576
|
+
registerHandlers();
|
|
577
|
+
render();
|
|
578
|
+
resolve();
|
|
579
|
+
});
|
|
580
|
+
modal.key(['escape'], () => {
|
|
581
|
+
screen.remove(modal);
|
|
582
|
+
registerHandlers();
|
|
583
|
+
render();
|
|
584
|
+
resolve();
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// Show unsaved changes dialog
|
|
589
|
+
async function showUnsavedDialog() {
|
|
590
|
+
unregisterHandlers();
|
|
591
|
+
return new Promise((resolve) => {
|
|
592
|
+
const modal = createModal('Unsaved Changes', 10);
|
|
593
|
+
let selectedOption = 0;
|
|
594
|
+
const options = [
|
|
595
|
+
{ key: 'save', label: '[S]ave and exit' },
|
|
596
|
+
{ key: 'discard', label: '[D]iscard changes' },
|
|
597
|
+
{ key: 'continue', label: '[C]ontinue editing' },
|
|
598
|
+
];
|
|
599
|
+
function renderDialog() {
|
|
600
|
+
let content = '\n';
|
|
601
|
+
for (let i = 0; i < options.length; i++) {
|
|
602
|
+
const isSelected = i === selectedOption;
|
|
603
|
+
if (isSelected) {
|
|
604
|
+
content += ` {cyan-fg}► ${options[i].label}{/cyan-fg}\n`;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
content += ` ${options[i].label}\n`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
modal.setContent(content);
|
|
611
|
+
screen.render();
|
|
612
|
+
}
|
|
613
|
+
renderDialog();
|
|
614
|
+
modal.focus();
|
|
615
|
+
modal.key(['up', 'k'], () => {
|
|
616
|
+
selectedOption = Math.max(0, selectedOption - 1);
|
|
617
|
+
renderDialog();
|
|
618
|
+
});
|
|
619
|
+
modal.key(['down', 'j'], () => {
|
|
620
|
+
selectedOption = Math.min(options.length - 1, selectedOption + 1);
|
|
621
|
+
renderDialog();
|
|
622
|
+
});
|
|
623
|
+
modal.key(['enter'], () => {
|
|
624
|
+
screen.remove(modal);
|
|
625
|
+
registerHandlers();
|
|
626
|
+
resolve(options[selectedOption].key);
|
|
627
|
+
});
|
|
628
|
+
modal.key(['s', 'S'], () => {
|
|
629
|
+
screen.remove(modal);
|
|
630
|
+
registerHandlers();
|
|
631
|
+
resolve('save');
|
|
632
|
+
});
|
|
633
|
+
modal.key(['d', 'D'], () => {
|
|
634
|
+
screen.remove(modal);
|
|
635
|
+
registerHandlers();
|
|
636
|
+
resolve('discard');
|
|
637
|
+
});
|
|
638
|
+
modal.key(['c', 'C', 'escape'], () => {
|
|
639
|
+
screen.remove(modal);
|
|
640
|
+
registerHandlers();
|
|
641
|
+
resolve('continue');
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// Show restart confirmation dialog
|
|
646
|
+
async function showRestartDialog() {
|
|
647
|
+
unregisterHandlers();
|
|
648
|
+
return new Promise((resolve) => {
|
|
649
|
+
const modal = createModal('Server is Running', 10);
|
|
650
|
+
let selectedOption = 0;
|
|
651
|
+
const options = [
|
|
652
|
+
{ key: true, label: '[Y]es - Restart now' },
|
|
653
|
+
{ key: false, label: '[N]o - Apply later' },
|
|
654
|
+
];
|
|
655
|
+
function renderDialog() {
|
|
656
|
+
let content = '\n Restart to apply changes?\n\n';
|
|
657
|
+
for (let i = 0; i < options.length; i++) {
|
|
658
|
+
const isSelected = i === selectedOption;
|
|
659
|
+
if (isSelected) {
|
|
660
|
+
content += ` {cyan-fg}► ${options[i].label}{/cyan-fg}\n`;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
content += ` ${options[i].label}\n`;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
modal.setContent(content);
|
|
667
|
+
screen.render();
|
|
668
|
+
}
|
|
669
|
+
renderDialog();
|
|
670
|
+
modal.focus();
|
|
671
|
+
modal.key(['up', 'k'], () => {
|
|
672
|
+
selectedOption = Math.max(0, selectedOption - 1);
|
|
673
|
+
renderDialog();
|
|
674
|
+
});
|
|
675
|
+
modal.key(['down', 'j'], () => {
|
|
676
|
+
selectedOption = Math.min(options.length - 1, selectedOption + 1);
|
|
677
|
+
renderDialog();
|
|
678
|
+
});
|
|
679
|
+
modal.key(['enter'], () => {
|
|
680
|
+
screen.remove(modal);
|
|
681
|
+
registerHandlers();
|
|
682
|
+
resolve(options[selectedOption].key);
|
|
683
|
+
});
|
|
684
|
+
modal.key(['y', 'Y'], () => {
|
|
685
|
+
screen.remove(modal);
|
|
686
|
+
registerHandlers();
|
|
687
|
+
resolve(true);
|
|
688
|
+
});
|
|
689
|
+
modal.key(['n', 'N', 'escape'], () => {
|
|
690
|
+
screen.remove(modal);
|
|
691
|
+
registerHandlers();
|
|
692
|
+
resolve(false);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
// Show progress modal
|
|
697
|
+
function showProgress(message) {
|
|
698
|
+
const modal = createModal('Working', 6);
|
|
699
|
+
modal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
|
|
700
|
+
screen.render();
|
|
701
|
+
return modal;
|
|
702
|
+
}
|
|
703
|
+
// Show error message
|
|
704
|
+
async function showError(message) {
|
|
705
|
+
unregisterHandlers();
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
const modal = createModal('Error', 8);
|
|
708
|
+
modal.setContent(`\n {red-fg}❌ ${message}{/red-fg}\n\n {gray-fg}[Enter] Close{/gray-fg}`);
|
|
709
|
+
screen.render();
|
|
710
|
+
modal.focus();
|
|
711
|
+
modal.key(['enter', 'escape'], () => {
|
|
712
|
+
screen.remove(modal);
|
|
713
|
+
registerHandlers();
|
|
714
|
+
render();
|
|
715
|
+
resolve();
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
// Save changes
|
|
720
|
+
async function saveChanges() {
|
|
721
|
+
// Build updates object
|
|
722
|
+
const modelField = state.fields.find(f => f.key === 'model');
|
|
723
|
+
const hostField = state.fields.find(f => f.key === 'host');
|
|
724
|
+
const portField = state.fields.find(f => f.key === 'port');
|
|
725
|
+
const threadsField = state.fields.find(f => f.key === 'threads');
|
|
726
|
+
const ctxSizeField = state.fields.find(f => f.key === 'ctxSize');
|
|
727
|
+
const gpuLayersField = state.fields.find(f => f.key === 'gpuLayers');
|
|
728
|
+
const verboseField = state.fields.find(f => f.key === 'verbose');
|
|
729
|
+
const customFlagsField = state.fields.find(f => f.key === 'customFlags');
|
|
730
|
+
// Check for port conflict if port changed
|
|
731
|
+
if (portField.value !== portField.originalValue) {
|
|
732
|
+
const hasConflict = await port_manager_js_1.portManager.checkPortConflict(portField.value, server.id);
|
|
733
|
+
if (hasConflict) {
|
|
734
|
+
await showError(`Port ${portField.value} is already in use by another server`);
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Check if model changed (requires migration)
|
|
739
|
+
let newModelPath;
|
|
740
|
+
let newModelName;
|
|
741
|
+
let newServerId;
|
|
742
|
+
let isModelMigration = false;
|
|
743
|
+
if (modelField.value !== modelField.originalValue) {
|
|
744
|
+
const resolvedPath = await model_scanner_js_1.modelScanner.resolveModelPath(modelField.value);
|
|
745
|
+
if (!resolvedPath) {
|
|
746
|
+
await showError(`Model not found: ${modelField.value}`);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
newModelPath = resolvedPath;
|
|
750
|
+
newModelName = modelField.value;
|
|
751
|
+
newServerId = (0, server_config_js_1.sanitizeModelName)(modelField.value);
|
|
752
|
+
if (newServerId !== server.id) {
|
|
753
|
+
isModelMigration = true;
|
|
754
|
+
const existingServer = await state_manager_js_1.stateManager.loadServerConfig(newServerId);
|
|
755
|
+
if (existingServer) {
|
|
756
|
+
await showError(`Server ID "${newServerId}" already exists. Delete it first.`);
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Check if server is running and prompt for restart
|
|
762
|
+
const currentStatus = await status_checker_js_1.statusChecker.updateServerStatus(server);
|
|
763
|
+
const wasRunning = currentStatus.status === 'running';
|
|
764
|
+
let shouldRestart = false;
|
|
765
|
+
if (wasRunning) {
|
|
766
|
+
shouldRestart = await showRestartDialog();
|
|
767
|
+
}
|
|
768
|
+
// Show progress
|
|
769
|
+
const progressModal = showProgress('Saving configuration...');
|
|
770
|
+
try {
|
|
771
|
+
// Parse custom flags
|
|
772
|
+
const customFlags = customFlagsField.value
|
|
773
|
+
? customFlagsField.value.split(',').map((f) => f.trim()).filter((f) => f.length > 0)
|
|
774
|
+
: undefined;
|
|
775
|
+
if (isModelMigration && newServerId && newModelPath && newModelName) {
|
|
776
|
+
// Model migration path
|
|
777
|
+
if (wasRunning) {
|
|
778
|
+
progressModal.setContent('\n {cyan-fg}Stopping old server...{/cyan-fg}');
|
|
779
|
+
screen.render();
|
|
780
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
|
|
781
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
782
|
+
}
|
|
783
|
+
progressModal.setContent('\n {cyan-fg}Removing old configuration...{/cyan-fg}');
|
|
784
|
+
screen.render();
|
|
785
|
+
try {
|
|
786
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(server.plistPath);
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
// Plist might not exist
|
|
790
|
+
}
|
|
791
|
+
await state_manager_js_1.stateManager.deleteServerConfig(server.id);
|
|
792
|
+
// Create new config with new ID
|
|
793
|
+
const logsDir = (0, file_utils_js_1.getLogsDir)();
|
|
794
|
+
const plistDir = (0, file_utils_js_1.getLaunchAgentsDir)();
|
|
795
|
+
const newConfig = {
|
|
796
|
+
...server,
|
|
797
|
+
id: newServerId,
|
|
798
|
+
modelPath: newModelPath,
|
|
799
|
+
modelName: newModelName,
|
|
800
|
+
host: hostField.value,
|
|
801
|
+
port: portField.value,
|
|
802
|
+
threads: threadsField.value,
|
|
803
|
+
ctxSize: ctxSizeField.value,
|
|
804
|
+
gpuLayers: gpuLayersField.value,
|
|
805
|
+
verbose: verboseField.value,
|
|
806
|
+
customFlags: customFlags && customFlags.length > 0 ? customFlags : undefined,
|
|
807
|
+
label: `com.llama.${newServerId}`,
|
|
808
|
+
plistPath: path.join(plistDir, `com.llama.${newServerId}.plist`),
|
|
809
|
+
stdoutPath: path.join(logsDir, `${newServerId}.stdout`),
|
|
810
|
+
stderrPath: path.join(logsDir, `${newServerId}.stderr`),
|
|
811
|
+
status: 'stopped',
|
|
812
|
+
pid: undefined,
|
|
813
|
+
lastStopped: new Date().toISOString(),
|
|
814
|
+
};
|
|
815
|
+
progressModal.setContent('\n {cyan-fg}Creating new configuration...{/cyan-fg}');
|
|
816
|
+
screen.render();
|
|
817
|
+
await state_manager_js_1.stateManager.saveServerConfig(newConfig);
|
|
818
|
+
await launchctl_manager_js_1.launchctlManager.createPlist(newConfig);
|
|
819
|
+
if (shouldRestart) {
|
|
820
|
+
progressModal.setContent('\n {cyan-fg}Starting new server...{/cyan-fg}');
|
|
821
|
+
screen.render();
|
|
822
|
+
await launchctl_manager_js_1.launchctlManager.loadService(newConfig.plistPath);
|
|
823
|
+
await launchctl_manager_js_1.launchctlManager.startService(newConfig.label);
|
|
824
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
825
|
+
const finalStatus = await status_checker_js_1.statusChecker.updateServerStatus(newConfig);
|
|
826
|
+
screen.remove(progressModal);
|
|
827
|
+
return finalStatus;
|
|
828
|
+
}
|
|
829
|
+
screen.remove(progressModal);
|
|
830
|
+
return newConfig;
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
// Normal config update (no migration)
|
|
834
|
+
if (wasRunning && shouldRestart) {
|
|
835
|
+
progressModal.setContent('\n {cyan-fg}Stopping server...{/cyan-fg}');
|
|
836
|
+
screen.render();
|
|
837
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
|
|
838
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
839
|
+
}
|
|
840
|
+
const updatedConfig = {
|
|
841
|
+
host: hostField.value,
|
|
842
|
+
port: portField.value,
|
|
843
|
+
threads: threadsField.value,
|
|
844
|
+
ctxSize: ctxSizeField.value,
|
|
845
|
+
gpuLayers: gpuLayersField.value,
|
|
846
|
+
verbose: verboseField.value,
|
|
847
|
+
customFlags: customFlags && customFlags.length > 0 ? customFlags : undefined,
|
|
848
|
+
};
|
|
849
|
+
if (newModelPath && newModelName) {
|
|
850
|
+
updatedConfig.modelPath = newModelPath;
|
|
851
|
+
updatedConfig.modelName = newModelName;
|
|
852
|
+
}
|
|
853
|
+
progressModal.setContent('\n {cyan-fg}Updating configuration...{/cyan-fg}');
|
|
854
|
+
screen.render();
|
|
855
|
+
await state_manager_js_1.stateManager.updateServerConfig(server.id, updatedConfig);
|
|
856
|
+
// Regenerate plist
|
|
857
|
+
const fullConfig = await state_manager_js_1.stateManager.loadServerConfig(server.id);
|
|
858
|
+
if (fullConfig) {
|
|
859
|
+
await launchctl_manager_js_1.launchctlManager.createPlist(fullConfig);
|
|
860
|
+
if (wasRunning && shouldRestart) {
|
|
861
|
+
// Auto-rotate logs if needed
|
|
862
|
+
try {
|
|
863
|
+
await (0, log_utils_js_1.autoRotateIfNeeded)(fullConfig.stdoutPath, fullConfig.stderrPath, 100);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
// Non-fatal
|
|
867
|
+
}
|
|
868
|
+
progressModal.setContent('\n {cyan-fg}Starting server...{/cyan-fg}');
|
|
869
|
+
screen.render();
|
|
870
|
+
await launchctl_manager_js_1.launchctlManager.loadService(fullConfig.plistPath);
|
|
871
|
+
await launchctl_manager_js_1.launchctlManager.startService(fullConfig.label);
|
|
872
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
873
|
+
const finalStatus = await status_checker_js_1.statusChecker.updateServerStatus(fullConfig);
|
|
874
|
+
screen.remove(progressModal);
|
|
875
|
+
return finalStatus;
|
|
876
|
+
}
|
|
877
|
+
screen.remove(progressModal);
|
|
878
|
+
return fullConfig;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
catch (err) {
|
|
883
|
+
screen.remove(progressModal);
|
|
884
|
+
await showError(err instanceof Error ? err.message : 'Unknown error');
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
screen.remove(progressModal);
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
// Handle edit action for selected field
|
|
891
|
+
async function handleEdit() {
|
|
892
|
+
const field = state.fields[state.selectedIndex];
|
|
893
|
+
switch (field.type) {
|
|
894
|
+
case 'number':
|
|
895
|
+
await editNumber(field);
|
|
896
|
+
break;
|
|
897
|
+
case 'toggle':
|
|
898
|
+
case 'select':
|
|
899
|
+
await editSelect(field);
|
|
900
|
+
break;
|
|
901
|
+
case 'text':
|
|
902
|
+
await editText(field);
|
|
903
|
+
break;
|
|
904
|
+
case 'model':
|
|
905
|
+
await editModel(field);
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// Handle escape/cancel
|
|
910
|
+
async function handleEscape() {
|
|
911
|
+
if (state.hasChanges) {
|
|
912
|
+
const result = await showUnsavedDialog();
|
|
913
|
+
if (result === 'save') {
|
|
914
|
+
const updated = await saveChanges();
|
|
915
|
+
cleanup();
|
|
916
|
+
onBack(updated || undefined);
|
|
917
|
+
}
|
|
918
|
+
else if (result === 'discard') {
|
|
919
|
+
cleanup();
|
|
920
|
+
onBack();
|
|
921
|
+
}
|
|
922
|
+
// 'continue' - just return to config screen
|
|
923
|
+
render();
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
cleanup();
|
|
927
|
+
onBack();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Handle save
|
|
931
|
+
async function handleSave() {
|
|
932
|
+
if (!state.hasChanges) {
|
|
933
|
+
cleanup();
|
|
934
|
+
onBack();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const updated = await saveChanges();
|
|
938
|
+
if (updated) {
|
|
939
|
+
cleanup();
|
|
940
|
+
onBack(updated);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
render();
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Key handlers
|
|
947
|
+
const keyHandlers = {
|
|
948
|
+
up: () => {
|
|
949
|
+
state.selectedIndex = Math.max(0, state.selectedIndex - 1);
|
|
950
|
+
render();
|
|
951
|
+
},
|
|
952
|
+
down: () => {
|
|
953
|
+
state.selectedIndex = Math.min(state.fields.length - 1, state.selectedIndex + 1);
|
|
954
|
+
render();
|
|
955
|
+
},
|
|
956
|
+
enter: () => {
|
|
957
|
+
handleEdit();
|
|
958
|
+
},
|
|
959
|
+
save: () => {
|
|
960
|
+
handleSave();
|
|
961
|
+
},
|
|
962
|
+
escape: () => {
|
|
963
|
+
handleEscape();
|
|
964
|
+
},
|
|
965
|
+
quit: () => {
|
|
966
|
+
screen.destroy();
|
|
967
|
+
process.exit(0);
|
|
968
|
+
},
|
|
969
|
+
};
|
|
970
|
+
// Unregister handlers (for modal dialogs)
|
|
971
|
+
function unregisterHandlers() {
|
|
972
|
+
screen.unkey('up', keyHandlers.up);
|
|
973
|
+
screen.unkey('k', keyHandlers.up);
|
|
974
|
+
screen.unkey('down', keyHandlers.down);
|
|
975
|
+
screen.unkey('j', keyHandlers.down);
|
|
976
|
+
screen.unkey('enter', keyHandlers.enter);
|
|
977
|
+
screen.unkey('s', keyHandlers.save);
|
|
978
|
+
screen.unkey('S', keyHandlers.save);
|
|
979
|
+
screen.unkey('escape', keyHandlers.escape);
|
|
980
|
+
screen.unkey('q', keyHandlers.quit);
|
|
981
|
+
screen.unkey('Q', keyHandlers.quit);
|
|
982
|
+
}
|
|
983
|
+
// Register handlers
|
|
984
|
+
function registerHandlers() {
|
|
985
|
+
screen.key(['up', 'k'], keyHandlers.up);
|
|
986
|
+
screen.key(['down', 'j'], keyHandlers.down);
|
|
987
|
+
screen.key(['enter'], keyHandlers.enter);
|
|
988
|
+
screen.key(['s', 'S'], keyHandlers.save);
|
|
989
|
+
screen.key(['escape'], keyHandlers.escape);
|
|
990
|
+
screen.key(['q', 'Q'], keyHandlers.quit);
|
|
991
|
+
}
|
|
992
|
+
// Cleanup function (for exiting config screen)
|
|
993
|
+
function cleanup() {
|
|
994
|
+
unregisterHandlers();
|
|
995
|
+
screen.remove(contentBox);
|
|
996
|
+
}
|
|
997
|
+
// Initial registration
|
|
998
|
+
registerHandlers();
|
|
999
|
+
// Initial render
|
|
1000
|
+
render();
|
|
1001
|
+
}
|
|
1002
|
+
//# sourceMappingURL=ConfigApp.js.map
|