@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +249 -40
  3. package/dist/cli.js +154 -10
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/completion.d.ts +9 -0
  6. package/dist/commands/completion.d.ts.map +1 -0
  7. package/dist/commands/completion.js +83 -0
  8. package/dist/commands/completion.js.map +1 -0
  9. package/dist/commands/monitor.js +1 -1
  10. package/dist/commands/monitor.js.map +1 -1
  11. package/dist/commands/ps.d.ts +1 -3
  12. package/dist/commands/ps.d.ts.map +1 -1
  13. package/dist/commands/ps.js +36 -115
  14. package/dist/commands/ps.js.map +1 -1
  15. package/dist/commands/router/config.d.ts +11 -0
  16. package/dist/commands/router/config.d.ts.map +1 -0
  17. package/dist/commands/router/config.js +100 -0
  18. package/dist/commands/router/config.js.map +1 -0
  19. package/dist/commands/router/logs.d.ts +12 -0
  20. package/dist/commands/router/logs.d.ts.map +1 -0
  21. package/dist/commands/router/logs.js +238 -0
  22. package/dist/commands/router/logs.js.map +1 -0
  23. package/dist/commands/router/restart.d.ts +2 -0
  24. package/dist/commands/router/restart.d.ts.map +1 -0
  25. package/dist/commands/router/restart.js +39 -0
  26. package/dist/commands/router/restart.js.map +1 -0
  27. package/dist/commands/router/start.d.ts +2 -0
  28. package/dist/commands/router/start.d.ts.map +1 -0
  29. package/dist/commands/router/start.js +60 -0
  30. package/dist/commands/router/start.js.map +1 -0
  31. package/dist/commands/router/status.d.ts +2 -0
  32. package/dist/commands/router/status.d.ts.map +1 -0
  33. package/dist/commands/router/status.js +116 -0
  34. package/dist/commands/router/status.js.map +1 -0
  35. package/dist/commands/router/stop.d.ts +2 -0
  36. package/dist/commands/router/stop.d.ts.map +1 -0
  37. package/dist/commands/router/stop.js +36 -0
  38. package/dist/commands/router/stop.js.map +1 -0
  39. package/dist/commands/tui.d.ts +2 -0
  40. package/dist/commands/tui.d.ts.map +1 -0
  41. package/dist/commands/tui.js +27 -0
  42. package/dist/commands/tui.js.map +1 -0
  43. package/dist/lib/completion.d.ts +5 -0
  44. package/dist/lib/completion.d.ts.map +1 -0
  45. package/dist/lib/completion.js +195 -0
  46. package/dist/lib/completion.js.map +1 -0
  47. package/dist/lib/model-downloader.d.ts +5 -1
  48. package/dist/lib/model-downloader.d.ts.map +1 -1
  49. package/dist/lib/model-downloader.js +53 -20
  50. package/dist/lib/model-downloader.js.map +1 -1
  51. package/dist/lib/router-logger.d.ts +61 -0
  52. package/dist/lib/router-logger.d.ts.map +1 -0
  53. package/dist/lib/router-logger.js +200 -0
  54. package/dist/lib/router-logger.js.map +1 -0
  55. package/dist/lib/router-manager.d.ts +103 -0
  56. package/dist/lib/router-manager.d.ts.map +1 -0
  57. package/dist/lib/router-manager.js +394 -0
  58. package/dist/lib/router-manager.js.map +1 -0
  59. package/dist/lib/router-server.d.ts +61 -0
  60. package/dist/lib/router-server.d.ts.map +1 -0
  61. package/dist/lib/router-server.js +485 -0
  62. package/dist/lib/router-server.js.map +1 -0
  63. package/dist/tui/ConfigApp.d.ts +7 -0
  64. package/dist/tui/ConfigApp.d.ts.map +1 -0
  65. package/dist/tui/ConfigApp.js +1002 -0
  66. package/dist/tui/ConfigApp.js.map +1 -0
  67. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  68. package/dist/tui/HistoricalMonitorApp.js +85 -49
  69. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  70. package/dist/tui/ModelsApp.d.ts +7 -0
  71. package/dist/tui/ModelsApp.d.ts.map +1 -0
  72. package/dist/tui/ModelsApp.js +362 -0
  73. package/dist/tui/ModelsApp.js.map +1 -0
  74. package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
  75. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  76. package/dist/tui/MultiServerMonitorApp.js +1038 -122
  77. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  78. package/dist/tui/RootNavigator.d.ts +7 -0
  79. package/dist/tui/RootNavigator.d.ts.map +1 -0
  80. package/dist/tui/RootNavigator.js +55 -0
  81. package/dist/tui/RootNavigator.js.map +1 -0
  82. package/dist/tui/SearchApp.d.ts +6 -0
  83. package/dist/tui/SearchApp.d.ts.map +1 -0
  84. package/dist/tui/SearchApp.js +451 -0
  85. package/dist/tui/SearchApp.js.map +1 -0
  86. package/dist/tui/SplashScreen.d.ts +16 -0
  87. package/dist/tui/SplashScreen.d.ts.map +1 -0
  88. package/dist/tui/SplashScreen.js +129 -0
  89. package/dist/tui/SplashScreen.js.map +1 -0
  90. package/dist/types/router-config.d.ts +19 -0
  91. package/dist/types/router-config.d.ts.map +1 -0
  92. package/dist/types/router-config.js +3 -0
  93. package/dist/types/router-config.js.map +1 -0
  94. package/package.json +1 -1
  95. package/src/cli.ts +121 -10
  96. package/src/commands/monitor.ts +1 -1
  97. package/src/commands/ps.ts +44 -133
  98. package/src/commands/router/config.ts +116 -0
  99. package/src/commands/router/logs.ts +256 -0
  100. package/src/commands/router/restart.ts +36 -0
  101. package/src/commands/router/start.ts +60 -0
  102. package/src/commands/router/status.ts +119 -0
  103. package/src/commands/router/stop.ts +33 -0
  104. package/src/commands/tui.ts +25 -0
  105. package/src/lib/model-downloader.ts +57 -20
  106. package/src/lib/router-logger.ts +201 -0
  107. package/src/lib/router-manager.ts +414 -0
  108. package/src/lib/router-server.ts +538 -0
  109. package/src/tui/ConfigApp.ts +1085 -0
  110. package/src/tui/HistoricalMonitorApp.ts +88 -49
  111. package/src/tui/ModelsApp.ts +368 -0
  112. package/src/tui/MultiServerMonitorApp.ts +1163 -122
  113. package/src/tui/RootNavigator.ts +74 -0
  114. package/src/tui/SearchApp.ts +511 -0
  115. package/src/tui/SplashScreen.ts +149 -0
  116. 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