@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +171 -42
  3. package/dist/cli.js +75 -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 +1 -0
  16. package/dist/commands/router/config.d.ts.map +1 -1
  17. package/dist/commands/router/config.js +7 -2
  18. package/dist/commands/router/config.js.map +1 -1
  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/tui.d.ts +2 -0
  24. package/dist/commands/tui.d.ts.map +1 -0
  25. package/dist/commands/tui.js +27 -0
  26. package/dist/commands/tui.js.map +1 -0
  27. package/dist/lib/completion.d.ts +5 -0
  28. package/dist/lib/completion.d.ts.map +1 -0
  29. package/dist/lib/completion.js +195 -0
  30. package/dist/lib/completion.js.map +1 -0
  31. package/dist/lib/model-downloader.d.ts +5 -1
  32. package/dist/lib/model-downloader.d.ts.map +1 -1
  33. package/dist/lib/model-downloader.js +53 -20
  34. package/dist/lib/model-downloader.js.map +1 -1
  35. package/dist/lib/router-logger.d.ts +61 -0
  36. package/dist/lib/router-logger.d.ts.map +1 -0
  37. package/dist/lib/router-logger.js +200 -0
  38. package/dist/lib/router-logger.js.map +1 -0
  39. package/dist/lib/router-manager.d.ts.map +1 -1
  40. package/dist/lib/router-manager.js +1 -0
  41. package/dist/lib/router-manager.js.map +1 -1
  42. package/dist/lib/router-server.d.ts +9 -0
  43. package/dist/lib/router-server.d.ts.map +1 -1
  44. package/dist/lib/router-server.js +169 -57
  45. package/dist/lib/router-server.js.map +1 -1
  46. package/dist/tui/ConfigApp.d.ts +7 -0
  47. package/dist/tui/ConfigApp.d.ts.map +1 -0
  48. package/dist/tui/ConfigApp.js +1002 -0
  49. package/dist/tui/ConfigApp.js.map +1 -0
  50. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  51. package/dist/tui/HistoricalMonitorApp.js +85 -49
  52. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  53. package/dist/tui/ModelsApp.d.ts +7 -0
  54. package/dist/tui/ModelsApp.d.ts.map +1 -0
  55. package/dist/tui/ModelsApp.js +362 -0
  56. package/dist/tui/ModelsApp.js.map +1 -0
  57. package/dist/tui/MultiServerMonitorApp.d.ts +6 -1
  58. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  59. package/dist/tui/MultiServerMonitorApp.js +1038 -122
  60. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  61. package/dist/tui/RootNavigator.d.ts +7 -0
  62. package/dist/tui/RootNavigator.d.ts.map +1 -0
  63. package/dist/tui/RootNavigator.js +55 -0
  64. package/dist/tui/RootNavigator.js.map +1 -0
  65. package/dist/tui/SearchApp.d.ts +6 -0
  66. package/dist/tui/SearchApp.d.ts.map +1 -0
  67. package/dist/tui/SearchApp.js +451 -0
  68. package/dist/tui/SearchApp.js.map +1 -0
  69. package/dist/tui/SplashScreen.d.ts +16 -0
  70. package/dist/tui/SplashScreen.d.ts.map +1 -0
  71. package/dist/tui/SplashScreen.js +129 -0
  72. package/dist/tui/SplashScreen.js.map +1 -0
  73. package/dist/types/router-config.d.ts +1 -0
  74. package/dist/types/router-config.d.ts.map +1 -1
  75. package/package.json +1 -1
  76. package/src/cli.ts +41 -10
  77. package/src/commands/monitor.ts +1 -1
  78. package/src/commands/ps.ts +44 -133
  79. package/src/commands/router/config.ts +9 -2
  80. package/src/commands/router/logs.ts +256 -0
  81. package/src/commands/tui.ts +25 -0
  82. package/src/lib/model-downloader.ts +57 -20
  83. package/src/lib/router-logger.ts +201 -0
  84. package/src/lib/router-manager.ts +1 -0
  85. package/src/lib/router-server.ts +193 -62
  86. package/src/tui/ConfigApp.ts +1085 -0
  87. package/src/tui/HistoricalMonitorApp.ts +88 -49
  88. package/src/tui/ModelsApp.ts +368 -0
  89. package/src/tui/MultiServerMonitorApp.ts +1163 -122
  90. package/src/tui/RootNavigator.ts +74 -0
  91. package/src/tui/SearchApp.ts +511 -0
  92. package/src/tui/SplashScreen.ts +149 -0
  93. 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
- async function createMultiServerMonitorUI(screen, servers, _fromPs = false, directJumpIndex) {
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}Model Resources{/bold}\n';
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}Model Resources{/bold}\n';
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 configuration (no metrics)
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
- content += '\n';
384
- content += '{bold}Configuration{/bold}\n';
385
- content += divider + '\n';
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}Updated: ${data.lastUpdated.toLocaleTimeString()} | [H]istory [ESC] Back [Q]uit{/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
- // Keyboard shortcuts - List view navigation with arrow keys
596
- screen.key(['up', 'k'], () => {
597
- if (viewMode === 'list') {
598
- selectedRowIndex = Math.max(0, selectedRowIndex - 1);
599
- // Re-render immediately for responsive feel
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
- screen.key(['down', 'j'], () => {
606
- if (viewMode === 'list') {
607
- selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
608
- // Re-render immediately for responsive feel
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
- // Enter key to view details for selected server
615
- screen.key(['enter'], () => {
616
- if (viewMode === 'list') {
617
- showLoading();
618
- selectedServerIndex = selectedRowIndex;
619
- viewMode = 'detail';
620
- fetchData();
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
- // Keyboard shortcuts - Detail view
624
- screen.key(['escape'], () => {
625
- // Don't handle ESC if we're in historical view - let historical view handle it
626
- if (inHistoricalView)
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
- else if (viewMode === 'list') {
635
- // ESC in list view - exit
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
- // Keyboard shortcuts - Common
648
- screen.key(['h', 'H'], async () => {
649
- // Prevent entering historical view if already there
650
- if (inHistoricalView)
651
- return;
652
- // Keep polling in background for live historical updates
653
- // Stop spinner if running
654
- if (spinnerIntervalId)
655
- clearInterval(spinnerIntervalId);
656
- // Remove current content box
657
- screen.remove(contentBox);
658
- // Mark that we're in historical view
659
- inHistoricalView = true;
660
- if (viewMode === 'list') {
661
- // Show multi-server historical view
662
- await (0, HistoricalMonitorApp_js_1.createMultiServerHistoricalUI)(screen, servers, selectedServerIndex, () => {
663
- // Mark that we've left historical view
664
- inHistoricalView = false;
665
- // Re-attach content box when returning from history
666
- screen.append(contentBox);
667
- // Re-render the list view
668
- const content = renderListView(lastSystemMetrics);
669
- contentBox.setContent(content);
670
- screen.render();
671
- });
672
- }
673
- else {
674
- // Show single-server historical view for selected server
675
- const selectedServer = servers[selectedServerIndex];
676
- await (0, HistoricalMonitorApp_js_1.createHistoricalUI)(screen, selectedServer, () => {
677
- // Mark that we've left historical view
678
- inHistoricalView = false;
679
- // Re-attach content box when returning from history
680
- screen.append(contentBox);
681
- // Re-render the detail view
682
- const content = renderDetailView(lastSystemMetrics);
683
- contentBox.setContent(content);
684
- screen.render();
685
- });
686
- }
687
- });
688
- screen.key(['q', 'Q', 'C-c'], () => {
689
- showLoading();
690
- if (intervalId)
691
- clearInterval(intervalId);
692
- if (spinnerIntervalId)
693
- clearInterval(spinnerIntervalId);
694
- // Small delay to show the loading state before exit
695
- setTimeout(() => {
696
- screen.destroy();
697
- process.exit(0);
698
- }, 100);
699
- });
700
- // Initial display
701
- contentBox.setContent('{cyan-fg}⏳ Connecting to servers...{/cyan-fg}');
702
- screen.render();
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