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