@appkit/llamacpp-cli 1.9.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 +16 -0
- package/README.md +171 -42
- package/dist/cli.js +75 -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 +1 -0
- package/dist/commands/router/config.d.ts.map +1 -1
- package/dist/commands/router/config.js +7 -2
- package/dist/commands/router/config.js.map +1 -1
- 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/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.map +1 -1
- package/dist/lib/router-manager.js +1 -0
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts +9 -0
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +169 -57
- package/dist/lib/router-server.js.map +1 -1
- 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 +1 -0
- package/dist/types/router-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +41 -10
- package/src/commands/monitor.ts +1 -1
- package/src/commands/ps.ts +44 -133
- package/src/commands/router/config.ts +9 -2
- package/src/commands/router/logs.ts +256 -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 +1 -0
- package/src/lib/router-server.ts +193 -62
- 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 +1 -0
|
@@ -38,11 +38,22 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.createMultiServerMonitorUI = createMultiServerMonitorUI;
|
|
40
40
|
const blessed_1 = __importDefault(require("blessed"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs/promises"));
|
|
41
43
|
const metrics_aggregator_js_1 = require("../lib/metrics-aggregator.js");
|
|
42
44
|
const system_collector_js_1 = require("../lib/system-collector.js");
|
|
43
45
|
const history_manager_js_1 = require("../lib/history-manager.js");
|
|
44
46
|
const HistoricalMonitorApp_js_1 = require("./HistoricalMonitorApp.js");
|
|
45
|
-
|
|
47
|
+
const ConfigApp_js_1 = require("./ConfigApp.js");
|
|
48
|
+
const state_manager_js_1 = require("../lib/state-manager.js");
|
|
49
|
+
const launchctl_manager_js_1 = require("../lib/launchctl-manager.js");
|
|
50
|
+
const status_checker_js_1 = require("../lib/status-checker.js");
|
|
51
|
+
const model_scanner_js_1 = require("../lib/model-scanner.js");
|
|
52
|
+
const port_manager_js_1 = require("../lib/port-manager.js");
|
|
53
|
+
const config_generator_js_1 = require("../lib/config-generator.js");
|
|
54
|
+
const file_utils_js_1 = require("../utils/file-utils.js");
|
|
55
|
+
const process_utils_js_1 = require("../utils/process-utils.js");
|
|
56
|
+
async function createMultiServerMonitorUI(screen, servers, skipConnectingMessage = false, directJumpIndex, onModels, onFirstRender) {
|
|
46
57
|
let updateInterval = 2000;
|
|
47
58
|
let intervalId = null;
|
|
48
59
|
let viewMode = directJumpIndex !== undefined ? 'detail' : 'list';
|
|
@@ -52,6 +63,7 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
52
63
|
let lastSystemMetrics = null;
|
|
53
64
|
let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
|
|
54
65
|
let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
|
|
66
|
+
let hasCalledFirstRender = false; // Track if we've called onFirstRender callback
|
|
55
67
|
// Spinner animation
|
|
56
68
|
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
57
69
|
let spinnerFrameIndex = 0;
|
|
@@ -140,7 +152,7 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
140
152
|
// Render aggregate model resources (all running servers in list view)
|
|
141
153
|
function renderAggregateModelResources() {
|
|
142
154
|
let content = '';
|
|
143
|
-
content += '{bold}
|
|
155
|
+
content += '{bold}Server Resources{/bold}\n';
|
|
144
156
|
const termWidth = screen.width || 80;
|
|
145
157
|
const divider = '─'.repeat(termWidth - 2);
|
|
146
158
|
content += divider + '\n';
|
|
@@ -180,7 +192,7 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
180
192
|
// Render model resources section (per-process for detail view)
|
|
181
193
|
function renderModelResources(data) {
|
|
182
194
|
let content = '';
|
|
183
|
-
content += '{bold}
|
|
195
|
+
content += '{bold}Server Resources{/bold}\n';
|
|
184
196
|
const termWidth = screen.width || 80;
|
|
185
197
|
const divider = '─'.repeat(termWidth - 2);
|
|
186
198
|
content += divider + '\n';
|
|
@@ -359,7 +371,7 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
359
371
|
});
|
|
360
372
|
// Footer
|
|
361
373
|
content += '\n' + divider + '\n';
|
|
362
|
-
content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [H]istory [Q]uit{/gray-fg}`;
|
|
374
|
+
content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [N]ew [M]odels [H]istory [Q]uit{/gray-fg}`;
|
|
363
375
|
return content;
|
|
364
376
|
}
|
|
365
377
|
// Render detail view for selected server
|
|
@@ -373,40 +385,16 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
373
385
|
content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
|
|
374
386
|
// Check if server is stopped
|
|
375
387
|
if (server.status !== 'running') {
|
|
376
|
-
// Show stopped server
|
|
388
|
+
// Show minimal stopped server info
|
|
377
389
|
content += '{bold}Server Information{/bold}\n';
|
|
378
390
|
content += divider + '\n';
|
|
379
391
|
content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
|
|
380
392
|
content += `Model: ${server.modelName}\n`;
|
|
381
393
|
const displayHost = server.host || '127.0.0.1';
|
|
382
394
|
content += `Endpoint: http://${displayHost}:${server.port}\n`;
|
|
383
|
-
|
|
384
|
-
content += '
|
|
385
|
-
content +=
|
|
386
|
-
content += `Threads: ${server.threads}\n`;
|
|
387
|
-
content += `Context: ${server.ctxSize} tokens\n`;
|
|
388
|
-
content += `GPU Layers: ${server.gpuLayers}\n`;
|
|
389
|
-
if (server.verbose) {
|
|
390
|
-
content += `Verbose: Enabled\n`;
|
|
391
|
-
}
|
|
392
|
-
if (server.customFlags && server.customFlags.length > 0) {
|
|
393
|
-
content += `Flags: ${server.customFlags.join(', ')}\n`;
|
|
394
|
-
}
|
|
395
|
-
content += '\n';
|
|
396
|
-
if (server.lastStarted) {
|
|
397
|
-
content += '{bold}Last Activity{/bold}\n';
|
|
398
|
-
content += divider + '\n';
|
|
399
|
-
content += `Started: ${new Date(server.lastStarted).toLocaleString()}\n`;
|
|
400
|
-
if (server.lastStopped) {
|
|
401
|
-
content += `Stopped: ${new Date(server.lastStopped).toLocaleString()}\n`;
|
|
402
|
-
}
|
|
403
|
-
content += '\n';
|
|
404
|
-
}
|
|
405
|
-
content += '{bold}Quick Actions{/bold}\n';
|
|
406
|
-
content += divider + '\n';
|
|
407
|
-
content += `{dim}Start server: llamacpp server start ${server.port}{/dim}\n`;
|
|
408
|
-
content += `{dim}Update config: llamacpp server config ${server.port} [options]{/dim}\n`;
|
|
409
|
-
content += `{dim}View logs: llamacpp server logs ${server.port}{/dim}\n`;
|
|
395
|
+
// Footer - show [S]tart for stopped servers
|
|
396
|
+
content += '\n' + divider + '\n';
|
|
397
|
+
content += `{gray-fg}[S]tart [C]onfig [R]emove [H]istory [ESC] Back [Q]uit{/gray-fg}`;
|
|
410
398
|
return content;
|
|
411
399
|
}
|
|
412
400
|
if (!serverData?.data) {
|
|
@@ -474,9 +462,9 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
474
462
|
content += '\n';
|
|
475
463
|
}
|
|
476
464
|
}
|
|
477
|
-
// Footer
|
|
465
|
+
// Footer - show [S]top for running servers
|
|
478
466
|
content += divider + '\n';
|
|
479
|
-
content += `{gray-fg}
|
|
467
|
+
content += `{gray-fg}[S]top [C]onfig [R]emove [H]istory [ESC] Back [Q]uit{/gray-fg}`;
|
|
480
468
|
return content;
|
|
481
469
|
}
|
|
482
470
|
// Fetch and update display
|
|
@@ -571,6 +559,11 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
571
559
|
content = renderDetailView(systemMetrics);
|
|
572
560
|
}
|
|
573
561
|
contentBox.setContent(content);
|
|
562
|
+
// Call onFirstRender callback before first render (to clean up splash screen)
|
|
563
|
+
if (!hasCalledFirstRender && onFirstRender) {
|
|
564
|
+
hasCalledFirstRender = true;
|
|
565
|
+
onFirstRender();
|
|
566
|
+
}
|
|
574
567
|
screen.render();
|
|
575
568
|
// Clear loading state
|
|
576
569
|
hideLoading();
|
|
@@ -592,114 +585,1036 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
592
585
|
fetchData();
|
|
593
586
|
intervalId = setInterval(fetchData, updateInterval);
|
|
594
587
|
}
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
588
|
+
// Parse context size with k/K suffix support (e.g., "4k" -> 4096, "64K" -> 65536)
|
|
589
|
+
function parseContextSize(input) {
|
|
590
|
+
const trimmed = input.trim().toLowerCase();
|
|
591
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(k)?$/);
|
|
592
|
+
if (!match)
|
|
593
|
+
return null;
|
|
594
|
+
const num = parseFloat(match[1]);
|
|
595
|
+
const hasK = match[2] === 'k';
|
|
596
|
+
if (isNaN(num) || num <= 0)
|
|
597
|
+
return null;
|
|
598
|
+
return hasK ? Math.round(num * 1024) : Math.round(num);
|
|
599
|
+
}
|
|
600
|
+
// Format context size for display (e.g., 4096 -> "4k", 65536 -> "64k")
|
|
601
|
+
function formatContextSize(value) {
|
|
602
|
+
if (value >= 1024 && value % 1024 === 0) {
|
|
603
|
+
return `${value / 1024}k`;
|
|
604
|
+
}
|
|
605
|
+
return value.toLocaleString();
|
|
606
|
+
}
|
|
607
|
+
// Helper to create modal boxes
|
|
608
|
+
function createModal(title, height = 'shrink', borderColor = 'cyan') {
|
|
609
|
+
return blessed_1.default.box({
|
|
610
|
+
parent: screen,
|
|
611
|
+
top: 'center',
|
|
612
|
+
left: 'center',
|
|
613
|
+
width: '70%',
|
|
614
|
+
height,
|
|
615
|
+
border: { type: 'line' },
|
|
616
|
+
style: {
|
|
617
|
+
border: { fg: borderColor },
|
|
618
|
+
fg: 'white',
|
|
619
|
+
},
|
|
620
|
+
tags: true,
|
|
621
|
+
label: ` ${title} `,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
// Show progress modal
|
|
625
|
+
function showProgressModal(message) {
|
|
626
|
+
const modal = createModal('Working', 6);
|
|
627
|
+
modal.setContent(`\n {cyan-fg}${message}{/cyan-fg}`);
|
|
628
|
+
screen.render();
|
|
629
|
+
return modal;
|
|
630
|
+
}
|
|
631
|
+
// Show error modal
|
|
632
|
+
async function showErrorModal(message) {
|
|
633
|
+
return new Promise((resolve) => {
|
|
634
|
+
const modal = createModal('Error', 8, 'red');
|
|
635
|
+
modal.setContent(`\n {red-fg}❌ ${message}{/red-fg}\n\n {gray-fg}[Enter] Close{/gray-fg}`);
|
|
636
|
+
screen.render();
|
|
637
|
+
modal.focus();
|
|
638
|
+
modal.key(['enter', 'escape'], () => {
|
|
639
|
+
screen.remove(modal);
|
|
640
|
+
resolve();
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
// Remove server dialog
|
|
645
|
+
async function showRemoveServerDialog(server) {
|
|
646
|
+
// Pause the monitor
|
|
647
|
+
if (intervalId)
|
|
648
|
+
clearInterval(intervalId);
|
|
649
|
+
if (spinnerIntervalId)
|
|
650
|
+
clearInterval(spinnerIntervalId);
|
|
651
|
+
unregisterHandlers();
|
|
652
|
+
// Check if other servers use the same model
|
|
653
|
+
const allServers = await state_manager_js_1.stateManager.getAllServers();
|
|
654
|
+
const otherServersWithSameModel = allServers.filter(s => s.id !== server.id && s.modelPath === server.modelPath);
|
|
655
|
+
let deleteModelOption = false;
|
|
656
|
+
const showDeleteModelOption = otherServersWithSameModel.length === 0;
|
|
657
|
+
// 0 = checkbox (delete model), 1 = confirm button
|
|
658
|
+
let selectedOption = showDeleteModelOption ? 0 : 1;
|
|
659
|
+
const modal = createModal('Remove Server', showDeleteModelOption ? 18 : 14, 'red');
|
|
660
|
+
function renderDialog() {
|
|
661
|
+
let content = '\n';
|
|
662
|
+
content += ` {bold}Remove server: ${server.id}{/bold}\n\n`;
|
|
663
|
+
content += ` Model: ${server.modelName}\n`;
|
|
664
|
+
content += ` Port: ${server.port}\n`;
|
|
665
|
+
content += ` Status: ${server.status === 'running' ? '{green-fg}running{/green-fg}' : '{gray-fg}stopped{/gray-fg}'}\n\n`;
|
|
666
|
+
if (server.status === 'running') {
|
|
667
|
+
content += ` {yellow-fg}⚠ Server will be stopped{/yellow-fg}\n\n`;
|
|
668
|
+
}
|
|
669
|
+
if (showDeleteModelOption) {
|
|
670
|
+
const checkbox = deleteModelOption ? '☑' : '☐';
|
|
671
|
+
const isCheckboxSelected = selectedOption === 0;
|
|
672
|
+
if (isCheckboxSelected) {
|
|
673
|
+
content += ` {cyan-bg}{15-fg}${checkbox} Also delete model file{/15-fg}{/cyan-bg}\n`;
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
content += ` ${checkbox} Also delete model file\n`;
|
|
677
|
+
}
|
|
678
|
+
content += ` {gray-fg}${server.modelPath}{/gray-fg}\n\n`;
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
content += ` {gray-fg}Model is used by ${otherServersWithSameModel.length} other server(s) - cannot delete{/gray-fg}\n\n`;
|
|
682
|
+
}
|
|
683
|
+
const isConfirmSelected = selectedOption === 1 || !showDeleteModelOption;
|
|
684
|
+
if (isConfirmSelected) {
|
|
685
|
+
content += ` {cyan-bg}{15-fg}[ Confirm Remove ]{/15-fg}{/cyan-bg}\n\n`;
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
content += ` [ Confirm Remove ]\n\n`;
|
|
689
|
+
}
|
|
690
|
+
content += ` {gray-fg}[↑/↓] Select [Space] Toggle [Enter] Confirm [ESC] Cancel{/gray-fg}`;
|
|
691
|
+
modal.setContent(content);
|
|
692
|
+
screen.render();
|
|
693
|
+
}
|
|
694
|
+
renderDialog();
|
|
695
|
+
modal.focus();
|
|
696
|
+
return new Promise((resolve) => {
|
|
697
|
+
modal.key(['up', 'k'], () => {
|
|
698
|
+
if (showDeleteModelOption && selectedOption === 1) {
|
|
699
|
+
selectedOption = 0;
|
|
700
|
+
renderDialog();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
modal.key(['down', 'j'], () => {
|
|
704
|
+
if (showDeleteModelOption && selectedOption === 0) {
|
|
705
|
+
selectedOption = 1;
|
|
706
|
+
renderDialog();
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
modal.key(['space'], () => {
|
|
710
|
+
if (showDeleteModelOption && selectedOption === 0) {
|
|
711
|
+
deleteModelOption = !deleteModelOption;
|
|
712
|
+
renderDialog();
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
modal.key(['escape'], () => {
|
|
716
|
+
screen.remove(modal);
|
|
717
|
+
registerHandlers();
|
|
718
|
+
startPolling();
|
|
719
|
+
resolve();
|
|
720
|
+
});
|
|
721
|
+
modal.key(['enter'], async () => {
|
|
722
|
+
screen.remove(modal);
|
|
723
|
+
// Show progress
|
|
724
|
+
const progressModal = showProgressModal('Removing server...');
|
|
725
|
+
try {
|
|
726
|
+
// Stop and unload service if running
|
|
727
|
+
if (server.status === 'running') {
|
|
728
|
+
progressModal.setContent('\n {cyan-fg}Stopping server...{/cyan-fg}');
|
|
729
|
+
screen.render();
|
|
730
|
+
try {
|
|
731
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
|
|
732
|
+
await launchctl_manager_js_1.launchctlManager.waitForServiceStop(server.label, 5000);
|
|
733
|
+
}
|
|
734
|
+
catch (err) {
|
|
735
|
+
// Continue even if unload fails
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Still try to unload in case it's in a weird state
|
|
740
|
+
try {
|
|
741
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(server.plistPath);
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
// Ignore
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Delete plist
|
|
748
|
+
progressModal.setContent('\n {cyan-fg}Removing configuration...{/cyan-fg}');
|
|
749
|
+
screen.render();
|
|
750
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(server.plistPath);
|
|
751
|
+
// Delete server config
|
|
752
|
+
await state_manager_js_1.stateManager.deleteServerConfig(server.id);
|
|
753
|
+
// Delete model if requested
|
|
754
|
+
if (deleteModelOption && showDeleteModelOption) {
|
|
755
|
+
progressModal.setContent('\n {cyan-fg}Deleting model file...{/cyan-fg}');
|
|
756
|
+
screen.render();
|
|
757
|
+
await fs.unlink(server.modelPath);
|
|
758
|
+
}
|
|
759
|
+
screen.remove(progressModal);
|
|
760
|
+
// Remove server from our arrays
|
|
761
|
+
const idx = servers.findIndex(s => s.id === server.id);
|
|
762
|
+
if (idx !== -1) {
|
|
763
|
+
servers.splice(idx, 1);
|
|
764
|
+
aggregators.delete(server.id);
|
|
765
|
+
historyManagers.delete(server.id);
|
|
766
|
+
serverDataMap.delete(server.id);
|
|
767
|
+
}
|
|
768
|
+
// Go back to list view
|
|
769
|
+
viewMode = 'list';
|
|
770
|
+
selectedRowIndex = Math.min(selectedRowIndex, Math.max(0, servers.length - 1));
|
|
771
|
+
selectedServerIndex = selectedRowIndex;
|
|
772
|
+
registerHandlers();
|
|
773
|
+
startPolling();
|
|
774
|
+
resolve();
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
screen.remove(progressModal);
|
|
778
|
+
await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
|
|
779
|
+
registerHandlers();
|
|
780
|
+
startPolling();
|
|
781
|
+
resolve();
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
// Create server flow
|
|
787
|
+
async function showCreateServerFlow() {
|
|
788
|
+
// Pause the monitor
|
|
789
|
+
if (intervalId)
|
|
790
|
+
clearInterval(intervalId);
|
|
791
|
+
if (spinnerIntervalId)
|
|
792
|
+
clearInterval(spinnerIntervalId);
|
|
793
|
+
unregisterHandlers();
|
|
794
|
+
// Step 1: Model selection
|
|
795
|
+
const models = await model_scanner_js_1.modelScanner.scanModels();
|
|
796
|
+
if (models.length === 0) {
|
|
797
|
+
await showErrorModal('No models found in ~/models directory.\nUse [M]odels → [S]earch to download models.');
|
|
798
|
+
// Immediately render with cached data for instant feedback
|
|
600
799
|
const content = renderListView(lastSystemMetrics);
|
|
601
800
|
contentBox.setContent(content);
|
|
602
801
|
screen.render();
|
|
802
|
+
registerHandlers();
|
|
803
|
+
startPolling();
|
|
804
|
+
return;
|
|
603
805
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
806
|
+
// Check which models already have servers
|
|
807
|
+
const allServers = await state_manager_js_1.stateManager.getAllServers();
|
|
808
|
+
const modelsWithServers = new Set(allServers.map(s => s.modelPath));
|
|
809
|
+
let selectedModelIndex = 0;
|
|
810
|
+
let scrollOffset = 0;
|
|
811
|
+
const maxVisible = 8;
|
|
812
|
+
const modelModal = createModal('Create Server - Select Model', maxVisible + 8);
|
|
813
|
+
function renderModelPicker() {
|
|
814
|
+
// Adjust scroll offset
|
|
815
|
+
if (selectedModelIndex < scrollOffset) {
|
|
816
|
+
scrollOffset = selectedModelIndex;
|
|
817
|
+
}
|
|
818
|
+
else if (selectedModelIndex >= scrollOffset + maxVisible) {
|
|
819
|
+
scrollOffset = selectedModelIndex - maxVisible + 1;
|
|
820
|
+
}
|
|
821
|
+
let content = '\n';
|
|
822
|
+
content += ' {bold}Select a model to create a server for:{/bold}\n\n';
|
|
823
|
+
const visibleModels = models.slice(scrollOffset, scrollOffset + maxVisible);
|
|
824
|
+
for (let i = 0; i < visibleModels.length; i++) {
|
|
825
|
+
const model = visibleModels[i];
|
|
826
|
+
const actualIndex = scrollOffset + i;
|
|
827
|
+
const isSelected = actualIndex === selectedModelIndex;
|
|
828
|
+
const hasServer = modelsWithServers.has(model.path);
|
|
829
|
+
const indicator = isSelected ? '►' : ' ';
|
|
830
|
+
// Truncate filename if too long
|
|
831
|
+
let displayName = model.filename;
|
|
832
|
+
const maxLen = 40;
|
|
833
|
+
if (displayName.length > maxLen) {
|
|
834
|
+
displayName = displayName.substring(0, maxLen - 3) + '...';
|
|
835
|
+
}
|
|
836
|
+
displayName = displayName.padEnd(maxLen);
|
|
837
|
+
const size = model.sizeFormatted.padStart(8);
|
|
838
|
+
const serverIndicator = hasServer ? ' {yellow-fg}(has server){/yellow-fg}' : '';
|
|
839
|
+
const serverIndicatorPlain = hasServer ? ' (has server)' : '';
|
|
840
|
+
if (isSelected) {
|
|
841
|
+
content += ` {cyan-bg}{15-fg}${indicator} ${displayName} ${size}${serverIndicatorPlain}{/15-fg}{/cyan-bg}\n`;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
content += ` ${indicator} ${displayName} {gray-fg}${size}{/gray-fg}${serverIndicator}\n`;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Scroll indicator
|
|
848
|
+
if (models.length > maxVisible) {
|
|
849
|
+
const scrollInfo = `${selectedModelIndex + 1}/${models.length}`;
|
|
850
|
+
content += `\n {gray-fg}${scrollInfo}{/gray-fg}`;
|
|
851
|
+
}
|
|
852
|
+
content += '\n\n {gray-fg}[↑/↓] Navigate [Enter] Select [ESC] Cancel{/gray-fg}';
|
|
853
|
+
modelModal.setContent(content);
|
|
854
|
+
screen.render();
|
|
855
|
+
}
|
|
856
|
+
renderModelPicker();
|
|
857
|
+
modelModal.focus();
|
|
858
|
+
const selectedModel = await new Promise((resolve) => {
|
|
859
|
+
modelModal.key(['up', 'k'], () => {
|
|
860
|
+
selectedModelIndex = Math.max(0, selectedModelIndex - 1);
|
|
861
|
+
renderModelPicker();
|
|
862
|
+
});
|
|
863
|
+
modelModal.key(['down', 'j'], () => {
|
|
864
|
+
selectedModelIndex = Math.min(models.length - 1, selectedModelIndex + 1);
|
|
865
|
+
renderModelPicker();
|
|
866
|
+
});
|
|
867
|
+
modelModal.key(['escape'], () => {
|
|
868
|
+
screen.remove(modelModal);
|
|
869
|
+
resolve(null);
|
|
870
|
+
});
|
|
871
|
+
modelModal.key(['enter'], () => {
|
|
872
|
+
screen.remove(modelModal);
|
|
873
|
+
resolve(models[selectedModelIndex]);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
if (!selectedModel) {
|
|
877
|
+
// Immediately render with cached data for instant feedback
|
|
609
878
|
const content = renderListView(lastSystemMetrics);
|
|
610
879
|
contentBox.setContent(content);
|
|
611
880
|
screen.render();
|
|
881
|
+
registerHandlers();
|
|
882
|
+
startPolling();
|
|
883
|
+
return;
|
|
612
884
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
885
|
+
// Create a non-null reference for closures
|
|
886
|
+
const model = selectedModel;
|
|
887
|
+
// Check if server already exists for this model
|
|
888
|
+
const existingServer = allServers.find(s => s.modelPath === model.path);
|
|
889
|
+
if (existingServer) {
|
|
890
|
+
await showErrorModal(`Server already exists for this model.\nServer ID: ${existingServer.id}\nPort: ${existingServer.port}`);
|
|
891
|
+
// Immediately render with cached data for instant feedback
|
|
892
|
+
const content = renderListView(lastSystemMetrics);
|
|
893
|
+
contentBox.setContent(content);
|
|
894
|
+
screen.render();
|
|
895
|
+
registerHandlers();
|
|
896
|
+
startPolling();
|
|
897
|
+
return;
|
|
621
898
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
//
|
|
626
|
-
|
|
899
|
+
// Generate smart defaults
|
|
900
|
+
const defaultPort = await port_manager_js_1.portManager.findAvailablePort();
|
|
901
|
+
const modelSize = model.size;
|
|
902
|
+
// Smart context size based on model size
|
|
903
|
+
let defaultCtxSize = 4096;
|
|
904
|
+
if (modelSize < 1024 * 1024 * 1024) { // < 1GB
|
|
905
|
+
defaultCtxSize = 2048;
|
|
906
|
+
}
|
|
907
|
+
else if (modelSize < 3 * 1024 * 1024 * 1024) { // < 3GB
|
|
908
|
+
defaultCtxSize = 4096;
|
|
909
|
+
}
|
|
910
|
+
else if (modelSize < 6 * 1024 * 1024 * 1024) { // < 6GB
|
|
911
|
+
defaultCtxSize = 8192;
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
defaultCtxSize = 16384;
|
|
915
|
+
}
|
|
916
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
917
|
+
const defaultThreads = Math.max(1, Math.floor(os.cpus().length / 2));
|
|
918
|
+
const config = {
|
|
919
|
+
host: '127.0.0.1',
|
|
920
|
+
port: defaultPort,
|
|
921
|
+
threads: defaultThreads,
|
|
922
|
+
ctxSize: defaultCtxSize,
|
|
923
|
+
gpuLayers: 60,
|
|
924
|
+
verbose: true,
|
|
925
|
+
};
|
|
926
|
+
// Configuration fields
|
|
927
|
+
const fields = [
|
|
928
|
+
{ key: 'host', label: 'Host', type: 'select', options: ['127.0.0.1', '0.0.0.0'] },
|
|
929
|
+
{ key: 'port', label: 'Port', type: 'number' },
|
|
930
|
+
{ key: 'threads', label: 'Threads', type: 'number' },
|
|
931
|
+
{ key: 'ctxSize', label: 'Context Size', type: 'number' },
|
|
932
|
+
{ key: 'gpuLayers', label: 'GPU Layers', type: 'number' },
|
|
933
|
+
{ key: 'verbose', label: 'Verbose Logs', type: 'toggle' },
|
|
934
|
+
];
|
|
935
|
+
let selectedFieldIndex = 0;
|
|
936
|
+
const configModal = createModal('Create Server - Configuration', 20);
|
|
937
|
+
function formatConfigValue(key, value) {
|
|
938
|
+
if (key === 'verbose')
|
|
939
|
+
return value ? 'Enabled' : 'Disabled';
|
|
940
|
+
if (key === 'ctxSize')
|
|
941
|
+
return formatContextSize(value);
|
|
942
|
+
return String(value);
|
|
943
|
+
}
|
|
944
|
+
function renderConfigScreen() {
|
|
945
|
+
let content = '\n';
|
|
946
|
+
content += ` {bold}Model:{/bold} ${model.filename}\n`;
|
|
947
|
+
content += ` {bold}Size:{/bold} ${model.sizeFormatted}\n\n`;
|
|
948
|
+
content += ' {bold}Server Configuration:{/bold}\n';
|
|
949
|
+
content += ' ─'.repeat(30) + '\n';
|
|
950
|
+
for (let i = 0; i < fields.length; i++) {
|
|
951
|
+
const field = fields[i];
|
|
952
|
+
const isSelected = i === selectedFieldIndex;
|
|
953
|
+
const indicator = isSelected ? '►' : ' ';
|
|
954
|
+
const label = field.label.padEnd(14);
|
|
955
|
+
const value = formatConfigValue(field.key, config[field.key]);
|
|
956
|
+
if (isSelected) {
|
|
957
|
+
content += ` {cyan-bg}{15-fg}${indicator} ${label}${value}{/15-fg}{/cyan-bg}\n`;
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
content += ` ${indicator} ${label}{cyan-fg}${value}{/cyan-fg}\n`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
content += '\n';
|
|
964
|
+
const createSelected = selectedFieldIndex === fields.length;
|
|
965
|
+
if (createSelected) {
|
|
966
|
+
content += ` {green-bg}{15-fg}[ Create Server ]{/15-fg}{/green-bg}\n`;
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
content += ` {green-fg}[ Create Server ]{/green-fg}\n`;
|
|
970
|
+
}
|
|
971
|
+
content += '\n {gray-fg}[↑/↓] Navigate [Enter] Edit/Create [ESC] Cancel{/gray-fg}';
|
|
972
|
+
configModal.setContent(content);
|
|
973
|
+
screen.render();
|
|
974
|
+
}
|
|
975
|
+
renderConfigScreen();
|
|
976
|
+
configModal.focus();
|
|
977
|
+
const shouldCreate = await new Promise((resolve) => {
|
|
978
|
+
configModal.key(['up', 'k'], () => {
|
|
979
|
+
selectedFieldIndex = Math.max(0, selectedFieldIndex - 1);
|
|
980
|
+
renderConfigScreen();
|
|
981
|
+
});
|
|
982
|
+
configModal.key(['down', 'j'], () => {
|
|
983
|
+
selectedFieldIndex = Math.min(fields.length, selectedFieldIndex + 1);
|
|
984
|
+
renderConfigScreen();
|
|
985
|
+
});
|
|
986
|
+
configModal.key(['escape'], () => {
|
|
987
|
+
screen.remove(configModal);
|
|
988
|
+
resolve(false);
|
|
989
|
+
});
|
|
990
|
+
configModal.key(['enter'], async () => {
|
|
991
|
+
if (selectedFieldIndex === fields.length) {
|
|
992
|
+
// Create button selected
|
|
993
|
+
screen.remove(configModal);
|
|
994
|
+
resolve(true);
|
|
995
|
+
}
|
|
996
|
+
else {
|
|
997
|
+
// Edit field
|
|
998
|
+
const field = fields[selectedFieldIndex];
|
|
999
|
+
if (field.type === 'select') {
|
|
1000
|
+
// Show select dialog
|
|
1001
|
+
const options = field.options;
|
|
1002
|
+
let optionIndex = options.indexOf(config[field.key]);
|
|
1003
|
+
if (optionIndex < 0)
|
|
1004
|
+
optionIndex = 0;
|
|
1005
|
+
const selectModal = createModal(field.label, options.length + 6);
|
|
1006
|
+
function renderSelectOptions() {
|
|
1007
|
+
let content = '\n';
|
|
1008
|
+
for (let i = 0; i < options.length; i++) {
|
|
1009
|
+
const isOpt = i === optionIndex;
|
|
1010
|
+
const ind = isOpt ? '●' : '○';
|
|
1011
|
+
if (isOpt) {
|
|
1012
|
+
content += ` {cyan-fg}${ind} ${options[i]}{/cyan-fg}\n`;
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
content += ` {gray-fg}${ind} ${options[i]}{/gray-fg}\n`;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (field.key === 'host' && options[optionIndex] === '0.0.0.0') {
|
|
1019
|
+
content += '\n {yellow-fg}⚠ Warning: Exposes server to network{/yellow-fg}';
|
|
1020
|
+
}
|
|
1021
|
+
content += '\n\n {gray-fg}[↑/↓] Select [Enter] Confirm{/gray-fg}';
|
|
1022
|
+
selectModal.setContent(content);
|
|
1023
|
+
screen.render();
|
|
1024
|
+
}
|
|
1025
|
+
renderSelectOptions();
|
|
1026
|
+
selectModal.focus();
|
|
1027
|
+
await new Promise((resolveSelect) => {
|
|
1028
|
+
selectModal.key(['up', 'k'], () => {
|
|
1029
|
+
optionIndex = Math.max(0, optionIndex - 1);
|
|
1030
|
+
renderSelectOptions();
|
|
1031
|
+
});
|
|
1032
|
+
selectModal.key(['down', 'j'], () => {
|
|
1033
|
+
optionIndex = Math.min(options.length - 1, optionIndex + 1);
|
|
1034
|
+
renderSelectOptions();
|
|
1035
|
+
});
|
|
1036
|
+
selectModal.key(['enter'], () => {
|
|
1037
|
+
config[field.key] = options[optionIndex];
|
|
1038
|
+
screen.remove(selectModal);
|
|
1039
|
+
resolveSelect();
|
|
1040
|
+
});
|
|
1041
|
+
selectModal.key(['escape'], () => {
|
|
1042
|
+
screen.remove(selectModal);
|
|
1043
|
+
resolveSelect();
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
renderConfigScreen();
|
|
1047
|
+
configModal.focus();
|
|
1048
|
+
}
|
|
1049
|
+
else if (field.type === 'toggle') {
|
|
1050
|
+
config[field.key] = !config[field.key];
|
|
1051
|
+
renderConfigScreen();
|
|
1052
|
+
}
|
|
1053
|
+
else if (field.type === 'number') {
|
|
1054
|
+
// Number input
|
|
1055
|
+
const isCtxSize = field.key === 'ctxSize';
|
|
1056
|
+
const inputModal = createModal(`Edit ${field.label}`, isCtxSize ? 11 : 10);
|
|
1057
|
+
const currentDisplay = isCtxSize
|
|
1058
|
+
? formatContextSize(config[field.key])
|
|
1059
|
+
: config[field.key];
|
|
1060
|
+
const infoText = blessed_1.default.text({
|
|
1061
|
+
parent: inputModal,
|
|
1062
|
+
top: 1,
|
|
1063
|
+
left: 2,
|
|
1064
|
+
content: `Current: ${currentDisplay}`,
|
|
1065
|
+
tags: true,
|
|
1066
|
+
});
|
|
1067
|
+
// Add hint for context size
|
|
1068
|
+
if (isCtxSize) {
|
|
1069
|
+
blessed_1.default.text({
|
|
1070
|
+
parent: inputModal,
|
|
1071
|
+
top: 2,
|
|
1072
|
+
left: 2,
|
|
1073
|
+
content: '{gray-fg}Accepts: 4096, 4k, 8k, 16k, 32k, 64k, 128k{/gray-fg}',
|
|
1074
|
+
tags: true,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const inputBox = blessed_1.default.textbox({
|
|
1078
|
+
parent: inputModal,
|
|
1079
|
+
top: isCtxSize ? 4 : 3,
|
|
1080
|
+
left: 2,
|
|
1081
|
+
right: 2,
|
|
1082
|
+
height: 3,
|
|
1083
|
+
inputOnFocus: true,
|
|
1084
|
+
border: { type: 'line' },
|
|
1085
|
+
style: {
|
|
1086
|
+
border: { fg: 'white' },
|
|
1087
|
+
focus: { border: { fg: 'green' } },
|
|
1088
|
+
},
|
|
1089
|
+
});
|
|
1090
|
+
blessed_1.default.text({
|
|
1091
|
+
parent: inputModal,
|
|
1092
|
+
bottom: 1,
|
|
1093
|
+
left: 2,
|
|
1094
|
+
content: '{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}',
|
|
1095
|
+
tags: true,
|
|
1096
|
+
});
|
|
1097
|
+
// Pre-fill with k notation for context size
|
|
1098
|
+
const initialValue = isCtxSize
|
|
1099
|
+
? formatContextSize(config[field.key])
|
|
1100
|
+
: String(config[field.key]);
|
|
1101
|
+
inputBox.setValue(initialValue);
|
|
1102
|
+
screen.render();
|
|
1103
|
+
inputBox.focus();
|
|
1104
|
+
await new Promise((resolveInput) => {
|
|
1105
|
+
inputBox.on('submit', (value) => {
|
|
1106
|
+
let numValue;
|
|
1107
|
+
if (isCtxSize) {
|
|
1108
|
+
numValue = parseContextSize(value);
|
|
1109
|
+
}
|
|
1110
|
+
else {
|
|
1111
|
+
numValue = parseInt(value, 10);
|
|
1112
|
+
if (isNaN(numValue))
|
|
1113
|
+
numValue = null;
|
|
1114
|
+
}
|
|
1115
|
+
if (numValue !== null && numValue > 0) {
|
|
1116
|
+
config[field.key] = numValue;
|
|
1117
|
+
}
|
|
1118
|
+
screen.remove(inputModal);
|
|
1119
|
+
resolveInput();
|
|
1120
|
+
});
|
|
1121
|
+
inputBox.on('cancel', () => {
|
|
1122
|
+
screen.remove(inputModal);
|
|
1123
|
+
resolveInput();
|
|
1124
|
+
});
|
|
1125
|
+
inputBox.key(['escape'], () => {
|
|
1126
|
+
screen.remove(inputModal);
|
|
1127
|
+
resolveInput();
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
renderConfigScreen();
|
|
1131
|
+
configModal.focus();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
if (!shouldCreate) {
|
|
1137
|
+
// Immediately render with cached data for instant feedback
|
|
1138
|
+
const content = renderListView(lastSystemMetrics);
|
|
1139
|
+
contentBox.setContent(content);
|
|
1140
|
+
screen.render();
|
|
1141
|
+
registerHandlers();
|
|
1142
|
+
startPolling();
|
|
627
1143
|
return;
|
|
628
|
-
if (viewMode === 'detail') {
|
|
629
|
-
showLoading();
|
|
630
|
-
viewMode = 'list';
|
|
631
|
-
cameFromDirectJump = false; // Clear direct jump flag when returning to list
|
|
632
|
-
fetchData();
|
|
633
1144
|
}
|
|
634
|
-
|
|
635
|
-
|
|
1145
|
+
// Step 3: Create the server
|
|
1146
|
+
const progressModal = showProgressModal('Creating server...');
|
|
1147
|
+
try {
|
|
1148
|
+
// Generate full server config
|
|
1149
|
+
const serverOptions = {
|
|
1150
|
+
port: config.port,
|
|
1151
|
+
host: config.host,
|
|
1152
|
+
threads: config.threads,
|
|
1153
|
+
ctxSize: config.ctxSize,
|
|
1154
|
+
gpuLayers: config.gpuLayers,
|
|
1155
|
+
verbose: config.verbose,
|
|
1156
|
+
};
|
|
1157
|
+
progressModal.setContent('\n {cyan-fg}Generating configuration...{/cyan-fg}');
|
|
1158
|
+
screen.render();
|
|
1159
|
+
const serverConfig = await config_generator_js_1.configGenerator.generateConfig(model.path, model.filename, model.size, config.port, serverOptions);
|
|
1160
|
+
// Ensure log directory exists
|
|
1161
|
+
await (0, file_utils_js_1.ensureDir)(path.dirname(serverConfig.stdoutPath));
|
|
1162
|
+
// Create plist
|
|
1163
|
+
progressModal.setContent('\n {cyan-fg}Creating launchctl service...{/cyan-fg}');
|
|
1164
|
+
screen.render();
|
|
1165
|
+
await launchctl_manager_js_1.launchctlManager.createPlist(serverConfig);
|
|
1166
|
+
// Load service
|
|
1167
|
+
try {
|
|
1168
|
+
await launchctl_manager_js_1.launchctlManager.loadService(serverConfig.plistPath);
|
|
1169
|
+
}
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
|
|
1172
|
+
throw new Error(`Failed to load service: ${error.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
// Start service
|
|
1175
|
+
progressModal.setContent('\n {cyan-fg}Starting server...{/cyan-fg}');
|
|
1176
|
+
screen.render();
|
|
1177
|
+
try {
|
|
1178
|
+
await launchctl_manager_js_1.launchctlManager.startService(serverConfig.label);
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
|
|
1182
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
|
|
1183
|
+
throw new Error(`Failed to start service: ${error.message}`);
|
|
1184
|
+
}
|
|
1185
|
+
// Wait for startup
|
|
1186
|
+
progressModal.setContent('\n {cyan-fg}Waiting for server to start...{/cyan-fg}');
|
|
1187
|
+
screen.render();
|
|
1188
|
+
const started = await launchctl_manager_js_1.launchctlManager.waitForServiceStart(serverConfig.label, 5000);
|
|
1189
|
+
if (!started) {
|
|
1190
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
|
|
1191
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
|
|
1192
|
+
throw new Error('Server failed to start. Check logs.');
|
|
1193
|
+
}
|
|
1194
|
+
// Wait for port to be ready (server may take a moment to bind)
|
|
1195
|
+
progressModal.setContent('\n {cyan-fg}Waiting for server to be ready...{/cyan-fg}');
|
|
1196
|
+
screen.render();
|
|
1197
|
+
const portTimeout = 10000; // 10 seconds
|
|
1198
|
+
const portStartTime = Date.now();
|
|
1199
|
+
let portReady = false;
|
|
1200
|
+
while (Date.now() - portStartTime < portTimeout) {
|
|
1201
|
+
if (await (0, process_utils_js_1.isPortInUse)(serverConfig.port)) {
|
|
1202
|
+
portReady = true;
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1206
|
+
}
|
|
1207
|
+
if (!portReady) {
|
|
1208
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(serverConfig.plistPath);
|
|
1209
|
+
await launchctl_manager_js_1.launchctlManager.deletePlist(serverConfig.plistPath);
|
|
1210
|
+
throw new Error('Server started but port not responding. Check logs.');
|
|
1211
|
+
}
|
|
1212
|
+
// Update config with running status
|
|
1213
|
+
let updatedConfig = await status_checker_js_1.statusChecker.updateServerStatus(serverConfig);
|
|
1214
|
+
// Parse Metal memory allocation (wait a bit for model to load)
|
|
1215
|
+
progressModal.setContent('\n {cyan-fg}Detecting GPU memory allocation...{/cyan-fg}');
|
|
1216
|
+
screen.render();
|
|
1217
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
1218
|
+
const metalMemoryMB = await (0, file_utils_js_1.parseMetalMemoryFromLog)(updatedConfig.stderrPath);
|
|
1219
|
+
if (metalMemoryMB) {
|
|
1220
|
+
updatedConfig = { ...updatedConfig, metalMemoryMB };
|
|
1221
|
+
}
|
|
1222
|
+
// Save server config
|
|
1223
|
+
await state_manager_js_1.stateManager.saveServerConfig(updatedConfig);
|
|
1224
|
+
// Show success message briefly
|
|
1225
|
+
progressModal.setContent('\n {green-fg}✓ Server created successfully!{/green-fg}');
|
|
1226
|
+
screen.render();
|
|
1227
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1228
|
+
screen.remove(progressModal);
|
|
1229
|
+
// Add to our arrays
|
|
1230
|
+
servers.push(updatedConfig);
|
|
1231
|
+
aggregators.set(updatedConfig.id, new metrics_aggregator_js_1.MetricsAggregator(updatedConfig));
|
|
1232
|
+
historyManagers.set(updatedConfig.id, new history_manager_js_1.HistoryManager(updatedConfig.id));
|
|
1233
|
+
serverDataMap.set(updatedConfig.id, {
|
|
1234
|
+
server: updatedConfig,
|
|
1235
|
+
data: null,
|
|
1236
|
+
error: null,
|
|
1237
|
+
});
|
|
1238
|
+
// Select the new server in list view
|
|
1239
|
+
selectedRowIndex = servers.length - 1;
|
|
1240
|
+
selectedServerIndex = selectedRowIndex;
|
|
1241
|
+
registerHandlers();
|
|
1242
|
+
startPolling();
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
screen.remove(progressModal);
|
|
1246
|
+
await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
|
|
1247
|
+
// Immediately render with cached data for instant feedback
|
|
1248
|
+
const content = renderListView(lastSystemMetrics);
|
|
1249
|
+
contentBox.setContent(content);
|
|
1250
|
+
screen.render();
|
|
1251
|
+
registerHandlers();
|
|
1252
|
+
startPolling();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// Store key handler references for cleanup when switching views
|
|
1256
|
+
const keyHandlers = {
|
|
1257
|
+
up: () => {
|
|
1258
|
+
if (viewMode === 'list') {
|
|
1259
|
+
selectedRowIndex = Math.max(0, selectedRowIndex - 1);
|
|
1260
|
+
// Re-render immediately for responsive feel
|
|
1261
|
+
const content = renderListView(lastSystemMetrics);
|
|
1262
|
+
contentBox.setContent(content);
|
|
1263
|
+
screen.render();
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
down: () => {
|
|
1267
|
+
if (viewMode === 'list') {
|
|
1268
|
+
selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
|
|
1269
|
+
// Re-render immediately for responsive feel
|
|
1270
|
+
const content = renderListView(lastSystemMetrics);
|
|
1271
|
+
contentBox.setContent(content);
|
|
1272
|
+
screen.render();
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
enter: () => {
|
|
1276
|
+
if (viewMode === 'list') {
|
|
1277
|
+
showLoading();
|
|
1278
|
+
selectedServerIndex = selectedRowIndex;
|
|
1279
|
+
viewMode = 'detail';
|
|
1280
|
+
fetchData();
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
escape: () => {
|
|
1284
|
+
// Don't handle ESC if we're in historical view - let historical view handle it
|
|
1285
|
+
if (inHistoricalView)
|
|
1286
|
+
return;
|
|
1287
|
+
if (viewMode === 'detail') {
|
|
1288
|
+
showLoading();
|
|
1289
|
+
viewMode = 'list';
|
|
1290
|
+
cameFromDirectJump = false; // Clear direct jump flag when returning to list
|
|
1291
|
+
fetchData();
|
|
1292
|
+
}
|
|
1293
|
+
else if (viewMode === 'list') {
|
|
1294
|
+
// ESC in list view - exit
|
|
1295
|
+
showLoading();
|
|
1296
|
+
if (intervalId)
|
|
1297
|
+
clearInterval(intervalId);
|
|
1298
|
+
if (spinnerIntervalId)
|
|
1299
|
+
clearInterval(spinnerIntervalId);
|
|
1300
|
+
setTimeout(() => {
|
|
1301
|
+
screen.destroy();
|
|
1302
|
+
process.exit(0);
|
|
1303
|
+
}, 100);
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
models: async () => {
|
|
1307
|
+
if (onModels && viewMode === 'list' && !inHistoricalView) {
|
|
1308
|
+
// Pause monitor (don't destroy - we'll resume when returning)
|
|
1309
|
+
controls.pause();
|
|
1310
|
+
await onModels(controls);
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
history: async () => {
|
|
1314
|
+
// Prevent entering historical view if already there
|
|
1315
|
+
if (inHistoricalView)
|
|
1316
|
+
return;
|
|
1317
|
+
// Keep polling in background for live historical updates
|
|
1318
|
+
// Stop spinner if running
|
|
1319
|
+
if (spinnerIntervalId)
|
|
1320
|
+
clearInterval(spinnerIntervalId);
|
|
1321
|
+
// Remove current content box
|
|
1322
|
+
screen.remove(contentBox);
|
|
1323
|
+
// Mark that we're in historical view
|
|
1324
|
+
inHistoricalView = true;
|
|
1325
|
+
if (viewMode === 'list') {
|
|
1326
|
+
// Show multi-server historical view
|
|
1327
|
+
await (0, HistoricalMonitorApp_js_1.createMultiServerHistoricalUI)(screen, servers, selectedServerIndex, () => {
|
|
1328
|
+
// Mark that we've left historical view
|
|
1329
|
+
inHistoricalView = false;
|
|
1330
|
+
// Re-attach content box when returning from history
|
|
1331
|
+
screen.append(contentBox);
|
|
1332
|
+
// Re-render the list view
|
|
1333
|
+
const content = renderListView(lastSystemMetrics);
|
|
1334
|
+
contentBox.setContent(content);
|
|
1335
|
+
screen.render();
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
// Show single-server historical view for selected server
|
|
1340
|
+
const selectedServer = servers[selectedServerIndex];
|
|
1341
|
+
await (0, HistoricalMonitorApp_js_1.createHistoricalUI)(screen, selectedServer, () => {
|
|
1342
|
+
// Mark that we've left historical view
|
|
1343
|
+
inHistoricalView = false;
|
|
1344
|
+
// Re-attach content box when returning from history
|
|
1345
|
+
screen.append(contentBox);
|
|
1346
|
+
// Re-render the detail view
|
|
1347
|
+
const content = renderDetailView(lastSystemMetrics);
|
|
1348
|
+
contentBox.setContent(content);
|
|
1349
|
+
screen.render();
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
},
|
|
1353
|
+
config: async () => {
|
|
1354
|
+
// Only available from detail view and not in historical view
|
|
1355
|
+
if (viewMode !== 'detail' || inHistoricalView)
|
|
1356
|
+
return;
|
|
1357
|
+
// Pause monitor
|
|
1358
|
+
controls.pause();
|
|
1359
|
+
const selectedServer = servers[selectedServerIndex];
|
|
1360
|
+
await (0, ConfigApp_js_1.createConfigUI)(screen, selectedServer, (updatedServer) => {
|
|
1361
|
+
if (updatedServer) {
|
|
1362
|
+
// Check if server ID changed (model migration)
|
|
1363
|
+
if (updatedServer.id !== selectedServer.id) {
|
|
1364
|
+
// Replace server in array and update aggregator/history manager
|
|
1365
|
+
servers[selectedServerIndex] = updatedServer;
|
|
1366
|
+
aggregators.delete(selectedServer.id);
|
|
1367
|
+
historyManagers.delete(selectedServer.id);
|
|
1368
|
+
serverDataMap.delete(selectedServer.id);
|
|
1369
|
+
aggregators.set(updatedServer.id, new metrics_aggregator_js_1.MetricsAggregator(updatedServer));
|
|
1370
|
+
historyManagers.set(updatedServer.id, new history_manager_js_1.HistoryManager(updatedServer.id));
|
|
1371
|
+
serverDataMap.set(updatedServer.id, {
|
|
1372
|
+
server: updatedServer,
|
|
1373
|
+
data: null,
|
|
1374
|
+
error: null,
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// Update server in place
|
|
1379
|
+
servers[selectedServerIndex] = updatedServer;
|
|
1380
|
+
serverDataMap.set(updatedServer.id, {
|
|
1381
|
+
server: updatedServer,
|
|
1382
|
+
data: null,
|
|
1383
|
+
error: null,
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
// Resume monitor
|
|
1388
|
+
controls.resume();
|
|
1389
|
+
});
|
|
1390
|
+
},
|
|
1391
|
+
remove: async () => {
|
|
1392
|
+
// Only available from detail view and not in historical view
|
|
1393
|
+
if (viewMode !== 'detail' || inHistoricalView)
|
|
1394
|
+
return;
|
|
1395
|
+
const selectedServer = servers[selectedServerIndex];
|
|
1396
|
+
// Show remove server dialog
|
|
1397
|
+
await showRemoveServerDialog(selectedServer);
|
|
1398
|
+
},
|
|
1399
|
+
startStop: async () => {
|
|
1400
|
+
// Only available from detail view and not in historical view
|
|
1401
|
+
if (viewMode !== 'detail' || inHistoricalView)
|
|
1402
|
+
return;
|
|
1403
|
+
const selectedServer = servers[selectedServerIndex];
|
|
1404
|
+
// If running, stop it. If stopped, start it.
|
|
1405
|
+
if (selectedServer.status === 'running') {
|
|
1406
|
+
// Stop the server
|
|
1407
|
+
if (intervalId)
|
|
1408
|
+
clearInterval(intervalId);
|
|
1409
|
+
if (spinnerIntervalId)
|
|
1410
|
+
clearInterval(spinnerIntervalId);
|
|
1411
|
+
unregisterHandlers();
|
|
1412
|
+
const progressModal = showProgressModal('Stopping server...');
|
|
1413
|
+
try {
|
|
1414
|
+
// Unload service (this stops and unregisters it)
|
|
1415
|
+
progressModal.setContent('\n {cyan-fg}Stopping server...{/cyan-fg}');
|
|
1416
|
+
screen.render();
|
|
1417
|
+
await launchctl_manager_js_1.launchctlManager.unloadService(selectedServer.plistPath);
|
|
1418
|
+
// Wait for shutdown
|
|
1419
|
+
progressModal.setContent('\n {cyan-fg}Waiting for server to stop...{/cyan-fg}');
|
|
1420
|
+
screen.render();
|
|
1421
|
+
await launchctl_manager_js_1.launchctlManager.waitForServiceStop(selectedServer.label, 5000);
|
|
1422
|
+
// Update server status
|
|
1423
|
+
const updatedServer = await status_checker_js_1.statusChecker.updateServerStatus(selectedServer);
|
|
1424
|
+
servers[selectedServerIndex] = updatedServer;
|
|
1425
|
+
serverDataMap.set(updatedServer.id, {
|
|
1426
|
+
server: updatedServer,
|
|
1427
|
+
data: null,
|
|
1428
|
+
error: null,
|
|
1429
|
+
});
|
|
1430
|
+
// Save updated config
|
|
1431
|
+
await state_manager_js_1.stateManager.saveServerConfig(updatedServer);
|
|
1432
|
+
// Show success briefly
|
|
1433
|
+
progressModal.setContent('\n {green-fg}✓ Server stopped successfully!{/green-fg}');
|
|
1434
|
+
screen.render();
|
|
1435
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
1436
|
+
screen.remove(progressModal);
|
|
1437
|
+
registerHandlers();
|
|
1438
|
+
startPolling();
|
|
1439
|
+
}
|
|
1440
|
+
catch (err) {
|
|
1441
|
+
screen.remove(progressModal);
|
|
1442
|
+
await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
|
|
1443
|
+
registerHandlers();
|
|
1444
|
+
startPolling();
|
|
1445
|
+
}
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
// Start the server
|
|
1449
|
+
// Pause the monitor
|
|
1450
|
+
if (intervalId)
|
|
1451
|
+
clearInterval(intervalId);
|
|
1452
|
+
if (spinnerIntervalId)
|
|
1453
|
+
clearInterval(spinnerIntervalId);
|
|
1454
|
+
unregisterHandlers();
|
|
1455
|
+
const progressModal = showProgressModal('Starting server...');
|
|
1456
|
+
try {
|
|
1457
|
+
// Recreate plist if needed
|
|
1458
|
+
const plistExists = await fs.access(selectedServer.plistPath).then(() => true).catch(() => false);
|
|
1459
|
+
if (!plistExists) {
|
|
1460
|
+
progressModal.setContent('\n {cyan-fg}Recreating plist...{/cyan-fg}');
|
|
1461
|
+
screen.render();
|
|
1462
|
+
await launchctl_manager_js_1.launchctlManager.createPlist(selectedServer);
|
|
1463
|
+
}
|
|
1464
|
+
// Load service
|
|
1465
|
+
progressModal.setContent('\n {cyan-fg}Loading service...{/cyan-fg}');
|
|
1466
|
+
screen.render();
|
|
1467
|
+
try {
|
|
1468
|
+
await launchctl_manager_js_1.launchctlManager.loadService(selectedServer.plistPath);
|
|
1469
|
+
}
|
|
1470
|
+
catch (err) {
|
|
1471
|
+
// May already be loaded, continue
|
|
1472
|
+
}
|
|
1473
|
+
// Start service
|
|
1474
|
+
progressModal.setContent('\n {cyan-fg}Starting server...{/cyan-fg}');
|
|
1475
|
+
screen.render();
|
|
1476
|
+
await launchctl_manager_js_1.launchctlManager.startService(selectedServer.label);
|
|
1477
|
+
// Wait for startup
|
|
1478
|
+
progressModal.setContent('\n {cyan-fg}Waiting for server to start...{/cyan-fg}');
|
|
1479
|
+
screen.render();
|
|
1480
|
+
const started = await launchctl_manager_js_1.launchctlManager.waitForServiceStart(selectedServer.label, 5000);
|
|
1481
|
+
if (!started) {
|
|
1482
|
+
throw new Error('Server failed to start. Check logs.');
|
|
1483
|
+
}
|
|
1484
|
+
// Wait for port to be ready
|
|
1485
|
+
progressModal.setContent('\n {cyan-fg}Waiting for server to be ready...{/cyan-fg}');
|
|
1486
|
+
screen.render();
|
|
1487
|
+
const portTimeout = 10000;
|
|
1488
|
+
const portStartTime = Date.now();
|
|
1489
|
+
let portReady = false;
|
|
1490
|
+
while (Date.now() - portStartTime < portTimeout) {
|
|
1491
|
+
if (await (0, process_utils_js_1.isPortInUse)(selectedServer.port)) {
|
|
1492
|
+
portReady = true;
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1496
|
+
}
|
|
1497
|
+
if (!portReady) {
|
|
1498
|
+
throw new Error('Server started but port not responding. Check logs.');
|
|
1499
|
+
}
|
|
1500
|
+
// Update server status
|
|
1501
|
+
const updatedServer = await status_checker_js_1.statusChecker.updateServerStatus(selectedServer);
|
|
1502
|
+
servers[selectedServerIndex] = updatedServer;
|
|
1503
|
+
serverDataMap.set(updatedServer.id, {
|
|
1504
|
+
server: updatedServer,
|
|
1505
|
+
data: null,
|
|
1506
|
+
error: null,
|
|
1507
|
+
});
|
|
1508
|
+
// Save updated config
|
|
1509
|
+
await state_manager_js_1.stateManager.saveServerConfig(updatedServer);
|
|
1510
|
+
// Show success briefly
|
|
1511
|
+
progressModal.setContent('\n {green-fg}✓ Server started successfully!{/green-fg}');
|
|
1512
|
+
screen.render();
|
|
1513
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
1514
|
+
screen.remove(progressModal);
|
|
1515
|
+
registerHandlers();
|
|
1516
|
+
startPolling();
|
|
1517
|
+
}
|
|
1518
|
+
catch (err) {
|
|
1519
|
+
screen.remove(progressModal);
|
|
1520
|
+
await showErrorModal(err instanceof Error ? err.message : 'Unknown error');
|
|
1521
|
+
registerHandlers();
|
|
1522
|
+
startPolling();
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
create: async () => {
|
|
1526
|
+
// Only available from list view and not in historical view
|
|
1527
|
+
if (viewMode !== 'list' || inHistoricalView)
|
|
1528
|
+
return;
|
|
1529
|
+
// Show create server flow
|
|
1530
|
+
await showCreateServerFlow();
|
|
1531
|
+
},
|
|
1532
|
+
quit: () => {
|
|
636
1533
|
showLoading();
|
|
637
1534
|
if (intervalId)
|
|
638
1535
|
clearInterval(intervalId);
|
|
639
1536
|
if (spinnerIntervalId)
|
|
640
1537
|
clearInterval(spinnerIntervalId);
|
|
1538
|
+
// Small delay to show the loading state before exit
|
|
641
1539
|
setTimeout(() => {
|
|
642
1540
|
screen.destroy();
|
|
643
1541
|
process.exit(0);
|
|
644
1542
|
}, 100);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
screen.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
screen.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
1543
|
+
},
|
|
1544
|
+
};
|
|
1545
|
+
// Unregister all keyboard handlers
|
|
1546
|
+
function unregisterHandlers() {
|
|
1547
|
+
screen.unkey('up', keyHandlers.up);
|
|
1548
|
+
screen.unkey('k', keyHandlers.up);
|
|
1549
|
+
screen.unkey('down', keyHandlers.down);
|
|
1550
|
+
screen.unkey('j', keyHandlers.down);
|
|
1551
|
+
screen.unkey('enter', keyHandlers.enter);
|
|
1552
|
+
screen.unkey('escape', keyHandlers.escape);
|
|
1553
|
+
screen.unkey('m', keyHandlers.models);
|
|
1554
|
+
screen.unkey('M', keyHandlers.models);
|
|
1555
|
+
screen.unkey('h', keyHandlers.history);
|
|
1556
|
+
screen.unkey('H', keyHandlers.history);
|
|
1557
|
+
screen.unkey('c', keyHandlers.config);
|
|
1558
|
+
screen.unkey('C', keyHandlers.config);
|
|
1559
|
+
screen.unkey('r', keyHandlers.remove);
|
|
1560
|
+
screen.unkey('R', keyHandlers.remove);
|
|
1561
|
+
screen.unkey('s', keyHandlers.startStop);
|
|
1562
|
+
screen.unkey('S', keyHandlers.startStop);
|
|
1563
|
+
screen.unkey('n', keyHandlers.create);
|
|
1564
|
+
screen.unkey('N', keyHandlers.create);
|
|
1565
|
+
screen.unkey('q', keyHandlers.quit);
|
|
1566
|
+
screen.unkey('Q', keyHandlers.quit);
|
|
1567
|
+
screen.unkey('C-c', keyHandlers.quit);
|
|
1568
|
+
}
|
|
1569
|
+
// Register keyboard handlers
|
|
1570
|
+
function registerHandlers() {
|
|
1571
|
+
screen.key(['up', 'k'], keyHandlers.up);
|
|
1572
|
+
screen.key(['down', 'j'], keyHandlers.down);
|
|
1573
|
+
screen.key(['enter'], keyHandlers.enter);
|
|
1574
|
+
screen.key(['escape'], keyHandlers.escape);
|
|
1575
|
+
screen.key(['m', 'M'], keyHandlers.models);
|
|
1576
|
+
screen.key(['h', 'H'], keyHandlers.history);
|
|
1577
|
+
screen.key(['c', 'C'], keyHandlers.config);
|
|
1578
|
+
screen.key(['r', 'R'], keyHandlers.remove);
|
|
1579
|
+
screen.key(['s', 'S'], keyHandlers.startStop);
|
|
1580
|
+
screen.key(['n', 'N'], keyHandlers.create);
|
|
1581
|
+
screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
|
|
1582
|
+
}
|
|
1583
|
+
// Controls object for pause/resume from other views
|
|
1584
|
+
const controls = {
|
|
1585
|
+
pause: () => {
|
|
1586
|
+
unregisterHandlers();
|
|
1587
|
+
if (intervalId)
|
|
1588
|
+
clearInterval(intervalId);
|
|
1589
|
+
if (spinnerIntervalId)
|
|
1590
|
+
clearInterval(spinnerIntervalId);
|
|
1591
|
+
screen.remove(contentBox);
|
|
1592
|
+
},
|
|
1593
|
+
resume: () => {
|
|
1594
|
+
screen.append(contentBox);
|
|
1595
|
+
registerHandlers();
|
|
1596
|
+
// Re-render with last known data (instant, no loading)
|
|
1597
|
+
let content = '';
|
|
1598
|
+
if (viewMode === 'list') {
|
|
1599
|
+
content = renderListView(lastSystemMetrics);
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
content = renderDetailView(lastSystemMetrics);
|
|
1603
|
+
}
|
|
1604
|
+
contentBox.setContent(content);
|
|
1605
|
+
screen.render();
|
|
1606
|
+
// Resume polling
|
|
1607
|
+
startPolling();
|
|
1608
|
+
},
|
|
1609
|
+
getServers: () => servers,
|
|
1610
|
+
};
|
|
1611
|
+
// Initial registration
|
|
1612
|
+
registerHandlers();
|
|
1613
|
+
// Initial display - skip "Connecting" message when returning from another view
|
|
1614
|
+
if (!skipConnectingMessage) {
|
|
1615
|
+
contentBox.setContent('{cyan-fg}⏳ Connecting to servers...{/cyan-fg}');
|
|
1616
|
+
screen.render();
|
|
1617
|
+
}
|
|
703
1618
|
startPolling();
|
|
704
1619
|
// Cleanup
|
|
705
1620
|
screen.on('destroy', () => {
|
|
@@ -708,5 +1623,6 @@ async function createMultiServerMonitorUI(screen, servers, _fromPs = false, dire
|
|
|
708
1623
|
// Note: macmon child processes will automatically die when parent exits
|
|
709
1624
|
// since they're spawned with detached: false
|
|
710
1625
|
});
|
|
1626
|
+
return controls;
|
|
711
1627
|
}
|
|
712
1628
|
//# sourceMappingURL=MultiServerMonitorApp.js.map
|