@appkit/llamacpp-cli 1.14.1 → 2.1.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 (230) hide show
  1. package/README.md +276 -280
  2. package/dist/cli.js +133 -23
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/admin/config.d.ts +1 -1
  5. package/dist/commands/admin/config.js +5 -5
  6. package/dist/commands/admin/config.js.map +1 -1
  7. package/dist/commands/admin/log-config.d.ts +11 -0
  8. package/dist/commands/admin/log-config.d.ts.map +1 -0
  9. package/dist/commands/admin/log-config.js +159 -0
  10. package/dist/commands/admin/log-config.js.map +1 -0
  11. package/dist/commands/admin/logs.d.ts +2 -3
  12. package/dist/commands/admin/logs.d.ts.map +1 -1
  13. package/dist/commands/admin/logs.js +6 -48
  14. package/dist/commands/admin/logs.js.map +1 -1
  15. package/dist/commands/admin/status.d.ts.map +1 -1
  16. package/dist/commands/admin/status.js +1 -0
  17. package/dist/commands/admin/status.js.map +1 -1
  18. package/dist/commands/config.d.ts +1 -0
  19. package/dist/commands/config.d.ts.map +1 -1
  20. package/dist/commands/config.js +63 -196
  21. package/dist/commands/config.js.map +1 -1
  22. package/dist/commands/create.d.ts +3 -2
  23. package/dist/commands/create.d.ts.map +1 -1
  24. package/dist/commands/create.js +24 -97
  25. package/dist/commands/create.js.map +1 -1
  26. package/dist/commands/delete.d.ts.map +1 -1
  27. package/dist/commands/delete.js +7 -24
  28. package/dist/commands/delete.js.map +1 -1
  29. package/dist/commands/internal/server-wrapper.d.ts +15 -0
  30. package/dist/commands/internal/server-wrapper.d.ts.map +1 -0
  31. package/dist/commands/internal/server-wrapper.js +126 -0
  32. package/dist/commands/internal/server-wrapper.js.map +1 -0
  33. package/dist/commands/logs-all.d.ts +0 -2
  34. package/dist/commands/logs-all.d.ts.map +1 -1
  35. package/dist/commands/logs-all.js +1 -61
  36. package/dist/commands/logs-all.js.map +1 -1
  37. package/dist/commands/logs.d.ts +2 -5
  38. package/dist/commands/logs.d.ts.map +1 -1
  39. package/dist/commands/logs.js +104 -120
  40. package/dist/commands/logs.js.map +1 -1
  41. package/dist/commands/migrate-labels.d.ts +12 -0
  42. package/dist/commands/migrate-labels.d.ts.map +1 -0
  43. package/dist/commands/migrate-labels.js +160 -0
  44. package/dist/commands/migrate-labels.js.map +1 -0
  45. package/dist/commands/ps.d.ts.map +1 -1
  46. package/dist/commands/ps.js +2 -1
  47. package/dist/commands/ps.js.map +1 -1
  48. package/dist/commands/rm.d.ts.map +1 -1
  49. package/dist/commands/rm.js +22 -48
  50. package/dist/commands/rm.js.map +1 -1
  51. package/dist/commands/router/config.d.ts +1 -1
  52. package/dist/commands/router/config.js +6 -6
  53. package/dist/commands/router/config.js.map +1 -1
  54. package/dist/commands/router/logs.d.ts +2 -4
  55. package/dist/commands/router/logs.d.ts.map +1 -1
  56. package/dist/commands/router/logs.js +34 -189
  57. package/dist/commands/router/logs.js.map +1 -1
  58. package/dist/commands/router/status.d.ts.map +1 -1
  59. package/dist/commands/router/status.js +1 -0
  60. package/dist/commands/router/status.js.map +1 -1
  61. package/dist/commands/server-show.d.ts.map +1 -1
  62. package/dist/commands/server-show.js +3 -0
  63. package/dist/commands/server-show.js.map +1 -1
  64. package/dist/commands/start.d.ts.map +1 -1
  65. package/dist/commands/start.js +21 -72
  66. package/dist/commands/start.js.map +1 -1
  67. package/dist/commands/stop.d.ts.map +1 -1
  68. package/dist/commands/stop.js +10 -26
  69. package/dist/commands/stop.js.map +1 -1
  70. package/dist/launchers/llamacpp-admin +8 -0
  71. package/dist/launchers/llamacpp-router +8 -0
  72. package/dist/launchers/llamacpp-server +8 -0
  73. package/dist/lib/admin-manager.d.ts +4 -0
  74. package/dist/lib/admin-manager.d.ts.map +1 -1
  75. package/dist/lib/admin-manager.js +42 -18
  76. package/dist/lib/admin-manager.js.map +1 -1
  77. package/dist/lib/admin-server.d.ts +48 -1
  78. package/dist/lib/admin-server.d.ts.map +1 -1
  79. package/dist/lib/admin-server.js +632 -238
  80. package/dist/lib/admin-server.js.map +1 -1
  81. package/dist/lib/config-generator.d.ts +1 -0
  82. package/dist/lib/config-generator.d.ts.map +1 -1
  83. package/dist/lib/config-generator.js +12 -5
  84. package/dist/lib/config-generator.js.map +1 -1
  85. package/dist/lib/keyboard-manager.d.ts +162 -0
  86. package/dist/lib/keyboard-manager.d.ts.map +1 -0
  87. package/dist/lib/keyboard-manager.js +247 -0
  88. package/dist/lib/keyboard-manager.js.map +1 -0
  89. package/dist/lib/label-migration.d.ts +65 -0
  90. package/dist/lib/label-migration.d.ts.map +1 -0
  91. package/dist/lib/label-migration.js +458 -0
  92. package/dist/lib/label-migration.js.map +1 -0
  93. package/dist/lib/launchctl-manager.d.ts +9 -0
  94. package/dist/lib/launchctl-manager.d.ts.map +1 -1
  95. package/dist/lib/launchctl-manager.js +65 -19
  96. package/dist/lib/launchctl-manager.js.map +1 -1
  97. package/dist/lib/log-management-service.d.ts +51 -0
  98. package/dist/lib/log-management-service.d.ts.map +1 -0
  99. package/dist/lib/log-management-service.js +124 -0
  100. package/dist/lib/log-management-service.js.map +1 -0
  101. package/dist/lib/log-workers.d.ts +70 -0
  102. package/dist/lib/log-workers.d.ts.map +1 -0
  103. package/dist/lib/log-workers.js +217 -0
  104. package/dist/lib/log-workers.js.map +1 -0
  105. package/dist/lib/model-downloader.d.ts +9 -1
  106. package/dist/lib/model-downloader.d.ts.map +1 -1
  107. package/dist/lib/model-downloader.js +98 -1
  108. package/dist/lib/model-downloader.js.map +1 -1
  109. package/dist/lib/model-management-service.d.ts +60 -0
  110. package/dist/lib/model-management-service.d.ts.map +1 -0
  111. package/dist/lib/model-management-service.js +246 -0
  112. package/dist/lib/model-management-service.js.map +1 -0
  113. package/dist/lib/model-management-service.test.d.ts +2 -0
  114. package/dist/lib/model-management-service.test.d.ts.map +1 -0
  115. package/dist/lib/model-management-service.test.js.map +1 -0
  116. package/dist/lib/model-scanner.d.ts +15 -3
  117. package/dist/lib/model-scanner.d.ts.map +1 -1
  118. package/dist/lib/model-scanner.js +174 -17
  119. package/dist/lib/model-scanner.js.map +1 -1
  120. package/dist/lib/openapi-spec.d.ts +1335 -0
  121. package/dist/lib/openapi-spec.d.ts.map +1 -0
  122. package/dist/lib/openapi-spec.js +1017 -0
  123. package/dist/lib/openapi-spec.js.map +1 -0
  124. package/dist/lib/router-logger.d.ts +1 -1
  125. package/dist/lib/router-logger.d.ts.map +1 -1
  126. package/dist/lib/router-logger.js +13 -11
  127. package/dist/lib/router-logger.js.map +1 -1
  128. package/dist/lib/router-manager.d.ts +4 -0
  129. package/dist/lib/router-manager.d.ts.map +1 -1
  130. package/dist/lib/router-manager.js +30 -18
  131. package/dist/lib/router-manager.js.map +1 -1
  132. package/dist/lib/router-server.d.ts +4 -7
  133. package/dist/lib/router-server.d.ts.map +1 -1
  134. package/dist/lib/router-server.js +71 -182
  135. package/dist/lib/router-server.js.map +1 -1
  136. package/dist/lib/server-config-service.d.ts +51 -0
  137. package/dist/lib/server-config-service.d.ts.map +1 -0
  138. package/dist/lib/server-config-service.js +310 -0
  139. package/dist/lib/server-config-service.js.map +1 -0
  140. package/dist/lib/server-config-service.test.d.ts +2 -0
  141. package/dist/lib/server-config-service.test.d.ts.map +1 -0
  142. package/dist/lib/server-config-service.test.js.map +1 -0
  143. package/dist/lib/server-lifecycle-service.d.ts +172 -0
  144. package/dist/lib/server-lifecycle-service.d.ts.map +1 -0
  145. package/dist/lib/server-lifecycle-service.js +619 -0
  146. package/dist/lib/server-lifecycle-service.js.map +1 -0
  147. package/dist/lib/state-manager.d.ts +18 -1
  148. package/dist/lib/state-manager.d.ts.map +1 -1
  149. package/dist/lib/state-manager.js +51 -2
  150. package/dist/lib/state-manager.js.map +1 -1
  151. package/dist/lib/status-checker.d.ts +11 -4
  152. package/dist/lib/status-checker.d.ts.map +1 -1
  153. package/dist/lib/status-checker.js +34 -1
  154. package/dist/lib/status-checker.js.map +1 -1
  155. package/dist/lib/validation-service.d.ts +43 -0
  156. package/dist/lib/validation-service.d.ts.map +1 -0
  157. package/dist/lib/validation-service.js +112 -0
  158. package/dist/lib/validation-service.js.map +1 -0
  159. package/dist/lib/validation-service.test.d.ts +2 -0
  160. package/dist/lib/validation-service.test.d.ts.map +1 -0
  161. package/dist/lib/validation-service.test.js.map +1 -0
  162. package/dist/scripts/http-log-filter.sh +8 -0
  163. package/dist/tui/ConfigApp.d.ts.map +1 -1
  164. package/dist/tui/ConfigApp.js +222 -184
  165. package/dist/tui/ConfigApp.js.map +1 -1
  166. package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
  167. package/dist/tui/HistoricalMonitorApp.js +12 -0
  168. package/dist/tui/HistoricalMonitorApp.js.map +1 -1
  169. package/dist/tui/ModelsApp.d.ts.map +1 -1
  170. package/dist/tui/ModelsApp.js +93 -17
  171. package/dist/tui/ModelsApp.js.map +1 -1
  172. package/dist/tui/MonitorApp.d.ts.map +1 -1
  173. package/dist/tui/MonitorApp.js +1 -3
  174. package/dist/tui/MonitorApp.js.map +1 -1
  175. package/dist/tui/MultiServerMonitorApp.d.ts +3 -3
  176. package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
  177. package/dist/tui/MultiServerMonitorApp.js +724 -508
  178. package/dist/tui/MultiServerMonitorApp.js.map +1 -1
  179. package/dist/tui/RootNavigator.d.ts.map +1 -1
  180. package/dist/tui/RootNavigator.js +17 -1
  181. package/dist/tui/RootNavigator.js.map +1 -1
  182. package/dist/tui/RouterApp.d.ts +6 -0
  183. package/dist/tui/RouterApp.d.ts.map +1 -0
  184. package/dist/tui/RouterApp.js +928 -0
  185. package/dist/tui/RouterApp.js.map +1 -0
  186. package/dist/tui/SearchApp.d.ts.map +1 -1
  187. package/dist/tui/SearchApp.js +27 -6
  188. package/dist/tui/SearchApp.js.map +1 -1
  189. package/dist/tui/shared/modal-controller.d.ts +65 -0
  190. package/dist/tui/shared/modal-controller.d.ts.map +1 -0
  191. package/dist/tui/shared/modal-controller.js +625 -0
  192. package/dist/tui/shared/modal-controller.js.map +1 -0
  193. package/dist/tui/shared/overlay-utils.d.ts +7 -0
  194. package/dist/tui/shared/overlay-utils.d.ts.map +1 -0
  195. package/dist/tui/shared/overlay-utils.js +54 -0
  196. package/dist/tui/shared/overlay-utils.js.map +1 -0
  197. package/dist/types/admin-config.d.ts +15 -2
  198. package/dist/types/admin-config.d.ts.map +1 -1
  199. package/dist/types/model-info.d.ts +5 -0
  200. package/dist/types/model-info.d.ts.map +1 -1
  201. package/dist/types/router-config.d.ts +2 -2
  202. package/dist/types/router-config.d.ts.map +1 -1
  203. package/dist/types/server-config.d.ts +8 -0
  204. package/dist/types/server-config.d.ts.map +1 -1
  205. package/dist/types/server-config.js +25 -0
  206. package/dist/types/server-config.js.map +1 -1
  207. package/dist/utils/http-log-filter.d.ts +10 -0
  208. package/dist/utils/http-log-filter.d.ts.map +1 -0
  209. package/dist/utils/http-log-filter.js +84 -0
  210. package/dist/utils/http-log-filter.js.map +1 -0
  211. package/dist/utils/log-parser.d.ts.map +1 -1
  212. package/dist/utils/log-parser.js +7 -4
  213. package/dist/utils/log-parser.js.map +1 -1
  214. package/dist/utils/log-utils.d.ts +59 -4
  215. package/dist/utils/log-utils.d.ts.map +1 -1
  216. package/dist/utils/log-utils.js +150 -11
  217. package/dist/utils/log-utils.js.map +1 -1
  218. package/dist/utils/shard-utils.d.ts +72 -0
  219. package/dist/utils/shard-utils.d.ts.map +1 -0
  220. package/dist/utils/shard-utils.js +168 -0
  221. package/dist/utils/shard-utils.js.map +1 -0
  222. package/package.json +18 -4
  223. package/src/launchers/llamacpp-admin +8 -0
  224. package/src/launchers/llamacpp-router +8 -0
  225. package/src/launchers/llamacpp-server +8 -0
  226. package/web/dist/assets/index-Byhoy86V.css +1 -0
  227. package/web/dist/assets/index-HSrgvray.js +50 -0
  228. package/web/dist/index.html +2 -2
  229. package/web/dist/assets/index-Bin89Lwr.css +0 -1
  230. package/web/dist/assets/index-CVmonw3T.js +0 -17
@@ -0,0 +1,928 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRouterUI = createRouterUI;
7
+ const blessed_1 = __importDefault(require("blessed"));
8
+ const router_manager_js_1 = require("../lib/router-manager.js");
9
+ const file_utils_js_1 = require("../utils/file-utils.js");
10
+ const log_utils_js_1 = require("../utils/log-utils.js");
11
+ const modal_controller_js_1 = require("./shared/modal-controller.js");
12
+ const keyboard_manager_js_1 = require("../lib/keyboard-manager.js");
13
+ /**
14
+ * Router management TUI
15
+ */
16
+ async function createRouterUI(screen, onBack) {
17
+ // Load initial config
18
+ const initialConfig = await router_manager_js_1.routerManager.loadConfig();
19
+ const initialStatus = initialConfig
20
+ ? await router_manager_js_1.routerManager.getServiceStatus(initialConfig.label)
21
+ : null;
22
+ // Initialize state
23
+ const state = {
24
+ config: initialConfig,
25
+ view: 'status',
26
+ fields: [],
27
+ selectedIndex: 0,
28
+ hasChanges: false,
29
+ isRunning: initialStatus?.isRunning || false,
30
+ pid: initialStatus?.pid || null,
31
+ logType: 'stdout',
32
+ logsLastUpdated: null,
33
+ logsRefreshInterval: null,
34
+ };
35
+ // Keyboard manager and modal controller for centralized keyboard handling
36
+ const keyboardManager = new keyboard_manager_js_1.KeyboardManager(screen);
37
+ const modalController = new modal_controller_js_1.ModalController(screen, keyboardManager);
38
+ // Initialize fields from config
39
+ if (initialConfig) {
40
+ state.fields = [
41
+ {
42
+ key: 'port',
43
+ label: 'Port',
44
+ type: 'number',
45
+ value: initialConfig.port,
46
+ originalValue: initialConfig.port,
47
+ validation: (value) => {
48
+ if (value < 1024)
49
+ return 'Port must be >= 1024';
50
+ if (value > 65535)
51
+ return 'Port must be <= 65535';
52
+ return null;
53
+ },
54
+ },
55
+ {
56
+ key: 'host',
57
+ label: 'Host',
58
+ type: 'select',
59
+ value: initialConfig.host,
60
+ originalValue: initialConfig.host,
61
+ options: ['127.0.0.1', '0.0.0.0'],
62
+ },
63
+ {
64
+ key: 'logging',
65
+ label: 'Logging',
66
+ type: 'toggle',
67
+ value: initialConfig.logging,
68
+ originalValue: initialConfig.logging,
69
+ options: ['Disabled', 'Enabled'],
70
+ },
71
+ {
72
+ key: 'healthCheckInterval',
73
+ label: 'Health Check (ms)',
74
+ type: 'number',
75
+ value: initialConfig.healthCheckInterval,
76
+ originalValue: initialConfig.healthCheckInterval,
77
+ validation: (value) => {
78
+ if (value < 1000)
79
+ return 'Must be at least 1000ms';
80
+ if (value > 60000)
81
+ return 'Must be at most 60000ms';
82
+ return null;
83
+ },
84
+ },
85
+ {
86
+ key: 'requestTimeout',
87
+ label: 'Request Timeout (ms)',
88
+ type: 'number',
89
+ value: initialConfig.requestTimeout,
90
+ originalValue: initialConfig.requestTimeout,
91
+ validation: (value) => {
92
+ if (value < 5000)
93
+ return 'Must be at least 5000ms';
94
+ if (value > 600000)
95
+ return 'Must be at most 600000ms';
96
+ return null;
97
+ },
98
+ },
99
+ ];
100
+ }
101
+ // Create content box
102
+ const contentBox = blessed_1.default.box({
103
+ top: 0,
104
+ left: 0,
105
+ width: '100%',
106
+ height: '100%',
107
+ tags: true,
108
+ scrollable: true,
109
+ alwaysScroll: true,
110
+ keys: true,
111
+ vi: true,
112
+ mouse: true,
113
+ scrollbar: {
114
+ ch: '█',
115
+ style: { fg: 'blue' },
116
+ },
117
+ });
118
+ screen.append(contentBox);
119
+ // Refresh status
120
+ async function refreshStatus() {
121
+ const config = await router_manager_js_1.routerManager.loadConfig();
122
+ state.config = config;
123
+ if (config) {
124
+ const status = await router_manager_js_1.routerManager.getServiceStatus(config.label);
125
+ state.isRunning = status.isRunning;
126
+ state.pid = status.pid;
127
+ }
128
+ }
129
+ // Check if any field has changed
130
+ function updateHasChanges() {
131
+ state.hasChanges = state.fields.some(f => f.value !== f.originalValue);
132
+ }
133
+ // Format display value for a field
134
+ function formatValue(field) {
135
+ if (field.type === 'toggle') {
136
+ return field.value ? 'Enabled' : 'Disabled';
137
+ }
138
+ if (field.type === 'number') {
139
+ // Port numbers should not have thousand separators
140
+ if (field.key === 'port') {
141
+ return String(field.value);
142
+ }
143
+ // Intervals and timeouts can have commas for readability
144
+ return field.value.toLocaleString();
145
+ }
146
+ return String(field.value);
147
+ }
148
+ // Render status view
149
+ function renderStatus() {
150
+ const termWidth = screen.width || 80;
151
+ const divider = '─'.repeat(termWidth - 2);
152
+ let content = '';
153
+ // Header
154
+ content += `{bold}{blue-fg}═══ Router Management{/blue-fg}{/bold}\n\n`;
155
+ if (!state.config) {
156
+ content += '{yellow-fg}Router not configured{/yellow-fg}\n';
157
+ content += 'Use [S]tart to create router configuration\n\n';
158
+ content += divider + '\n';
159
+ content += '{gray-fg}[S]tart [ESC] Back [Q]uit{/gray-fg}';
160
+ contentBox.setContent(content);
161
+ screen.render();
162
+ return;
163
+ }
164
+ // Status section
165
+ content += '{bold}Status{/bold}\n';
166
+ content += divider + '\n';
167
+ const statusColor = state.isRunning ? 'green' : 'gray';
168
+ const statusText = state.isRunning ? 'RUNNING' : 'STOPPED';
169
+ content += `Status: {${statusColor}-fg}${statusText}{/${statusColor}-fg}\n`;
170
+ if (state.pid) {
171
+ content += `PID: ${state.pid}\n`;
172
+ }
173
+ content += `URL: http://${state.config.host}:${state.config.port}\n`;
174
+ if (state.config.lastStarted) {
175
+ const lastStarted = new Date(state.config.lastStarted);
176
+ content += `Last Started: ${lastStarted.toLocaleString()}\n`;
177
+ }
178
+ if (state.config.lastStopped) {
179
+ const lastStopped = new Date(state.config.lastStopped);
180
+ content += `Last Stopped: ${lastStopped.toLocaleString()}\n`;
181
+ }
182
+ content += '\n';
183
+ // Configuration summary (read-only)
184
+ content += '{bold}Configuration{/bold}\n';
185
+ content += divider + '\n';
186
+ content += `Port: ${state.config.port}\n`;
187
+ content += `Host: ${state.config.host}\n`;
188
+ content += `Logging: ${state.config.logging ? 'Enabled' : 'Disabled'}\n`;
189
+ content += `Health Check Interval: ${state.config.healthCheckInterval.toLocaleString()}ms\n`;
190
+ content += `Request Timeout: ${state.config.requestTimeout.toLocaleString()}ms\n`;
191
+ content += '\n';
192
+ // Show unsaved changes warning if any
193
+ if (state.hasChanges) {
194
+ content += '{yellow-fg}⚠ Configuration has unsaved changes{/yellow-fg}\n';
195
+ content += 'Press [C]onfig to review and save\n\n';
196
+ }
197
+ // Footer
198
+ content += divider + '\n';
199
+ if (state.isRunning) {
200
+ content += '{gray-fg}[S]top [R]estart [C]onfig [L]ogs [ESC] Back [Q]uit{/gray-fg}';
201
+ }
202
+ else {
203
+ content += '{gray-fg}[S]tart [C]onfig [L]ogs [ESC] Back [Q]uit{/gray-fg}';
204
+ }
205
+ contentBox.setContent(content);
206
+ screen.render();
207
+ }
208
+ // Render config view
209
+ function renderConfig() {
210
+ const termWidth = screen.width || 80;
211
+ const divider = '─'.repeat(termWidth - 2);
212
+ let content = '';
213
+ // Header
214
+ content += `{bold}{blue-fg}═══ Router Configuration{/blue-fg}{/bold}\n\n`;
215
+ // Configuration fields
216
+ for (let i = 0; i < state.fields.length; i++) {
217
+ const field = state.fields[i];
218
+ const isSelected = i === state.selectedIndex;
219
+ const hasChanged = field.value !== field.originalValue;
220
+ const indicator = isSelected ? '►' : ' ';
221
+ const label = field.label.padEnd(20);
222
+ const value = formatValue(field);
223
+ // Color coding: cyan for selected, yellow for changed
224
+ let valueDisplay = value;
225
+ if (hasChanged) {
226
+ valueDisplay = `{yellow-fg}${value}{/yellow-fg}`;
227
+ }
228
+ if (isSelected) {
229
+ content += `{cyan-bg}{15-fg} ${indicator} ${label}${value}{/15-fg}{/cyan-bg}\n`;
230
+ }
231
+ else {
232
+ content += ` ${indicator} ${label}${valueDisplay}\n`;
233
+ }
234
+ }
235
+ content += '\n';
236
+ // Show changes summary if any
237
+ if (state.hasChanges) {
238
+ content += '{yellow-fg}* Unsaved Changes:{/yellow-fg}\n';
239
+ for (const field of state.fields) {
240
+ if (field.value !== field.originalValue) {
241
+ const oldVal = field.type === 'toggle'
242
+ ? (field.originalValue ? 'Enabled' : 'Disabled')
243
+ : field.originalValue;
244
+ const newVal = field.type === 'toggle'
245
+ ? (field.value ? 'Enabled' : 'Disabled')
246
+ : field.value;
247
+ content += ` ${field.label}: ${oldVal} → {yellow-fg}${newVal}{/yellow-fg}\n`;
248
+ }
249
+ }
250
+ content += '\n';
251
+ }
252
+ // Footer
253
+ content += divider + '\n';
254
+ content += '{gray-fg}[↑/↓] Navigate [Enter] Edit [S]ave [ESC] Back [Q]uit{/gray-fg}';
255
+ contentBox.setContent(content);
256
+ screen.render();
257
+ }
258
+ // Render logs view
259
+ async function renderLogs() {
260
+ if (!state.config)
261
+ return;
262
+ const termWidth = screen.width || 80;
263
+ const divider = '─'.repeat(termWidth - 2);
264
+ let content = '';
265
+ // Header
266
+ const logTypeLabel = state.logType === 'stdout' ? 'Activity' : 'System';
267
+ content += `{bold}{blue-fg}═══ Router Logs (${logTypeLabel}){/blue-fg}{/bold}\n`;
268
+ // Show refresh status
269
+ const refreshStatus = state.logsRefreshInterval ? 'ON' : 'OFF';
270
+ const refreshColor = state.logsRefreshInterval ? 'green' : 'gray';
271
+ content += `{gray-fg}Auto-refresh: {/${refreshColor}-fg}${refreshStatus}{/${refreshColor}-fg}{/gray-fg}\n\n`;
272
+ const logPath = state.logType === 'stdout' ? state.config.stdoutPath : state.config.stderrPath;
273
+ // Check if log exists
274
+ if (!(await (0, file_utils_js_1.fileExists)(logPath))) {
275
+ content += '{yellow-fg}No logs found{/yellow-fg}\n';
276
+ // Show notice if logging is disabled for Activity logs
277
+ if (state.logType === 'stdout' && !state.config.logging) {
278
+ content += `{yellow-fg}⚠ Logging is disabled{/yellow-fg}\n`;
279
+ }
280
+ content += `Log file: ${logPath}\n\n`;
281
+ content += divider + '\n';
282
+ content += '{gray-fg}[T]oggle activity/system [R]efresh [ESC] Back{/gray-fg}';
283
+ contentBox.setContent(content);
284
+ screen.render();
285
+ return;
286
+ }
287
+ // Show log size
288
+ const size = await (0, log_utils_js_1.getFileSize)(logPath);
289
+ content += `File: ${logPath}\n`;
290
+ content += `Size: ${(0, log_utils_js_1.formatFileSize)(size)}\n`;
291
+ // Show notice if logging is disabled for Activity logs
292
+ if (state.logType === 'stdout' && !state.config.logging) {
293
+ content += `{yellow-fg}⚠ Logging is disabled{/yellow-fg}\n`;
294
+ }
295
+ content += `\n`;
296
+ // Show last 30 lines (truncate to fit terminal width)
297
+ try {
298
+ const { execSync } = require('child_process');
299
+ const output = execSync(`tail -n 30 "${logPath}"`, { encoding: 'utf-8' });
300
+ const lines = output.split('\n').filter((l) => l).reverse();
301
+ content += divider + '\n';
302
+ // Calculate max width for log lines (account for borders and padding)
303
+ const maxWidth = termWidth - 4;
304
+ for (const line of lines) {
305
+ // Remove ANSI color codes to calculate visible length
306
+ const visibleLine = line.replace(/\x1b\[[0-9;]*m/g, '');
307
+ if (visibleLine.length > maxWidth) {
308
+ // Truncate and add ellipsis
309
+ // Keep ANSI codes by truncating the original line at the right position
310
+ let visibleLen = 0;
311
+ let truncated = '';
312
+ for (let i = 0; i < line.length && visibleLen < maxWidth - 3; i++) {
313
+ truncated += line[i];
314
+ // Only count visible characters
315
+ if (line[i] !== '\x1b' && !line.slice(i).match(/^\x1b\[[0-9;]*m/)) {
316
+ visibleLen++;
317
+ }
318
+ else if (line.slice(i).match(/^\x1b\[[0-9;]*m/)) {
319
+ // Skip the rest of the ANSI code
320
+ const match = line.slice(i).match(/^\x1b\[[0-9;]*m/);
321
+ if (match) {
322
+ truncated += match[0].slice(1);
323
+ i += match[0].length - 1;
324
+ }
325
+ }
326
+ }
327
+ content += truncated + '...\n';
328
+ }
329
+ else {
330
+ content += line + '\n';
331
+ }
332
+ }
333
+ content += divider + '\n';
334
+ }
335
+ catch (err) {
336
+ content += '{red-fg}Failed to read logs{/red-fg}\n';
337
+ }
338
+ const toggleRefreshText = state.logsRefreshInterval ? '[F] Pause auto-refresh' : '[F] Resume auto-refresh';
339
+ content += `{gray-fg}[T]oggle activity/system [R]efresh ${toggleRefreshText} [ESC] Back{/gray-fg}`;
340
+ // Update last updated time
341
+ state.logsLastUpdated = new Date();
342
+ contentBox.setContent(content);
343
+ screen.render();
344
+ }
345
+ // Start logs auto-refresh
346
+ function startLogsAutoRefresh() {
347
+ // Clear any existing interval
348
+ if (state.logsRefreshInterval) {
349
+ clearInterval(state.logsRefreshInterval);
350
+ }
351
+ // Set up auto-refresh every 3 seconds
352
+ state.logsRefreshInterval = setInterval(() => {
353
+ if (state.view === 'logs') {
354
+ renderLogs();
355
+ }
356
+ }, 3000);
357
+ }
358
+ // Stop logs auto-refresh
359
+ function stopLogsAutoRefresh() {
360
+ if (state.logsRefreshInterval) {
361
+ clearInterval(state.logsRefreshInterval);
362
+ state.logsRefreshInterval = null;
363
+ }
364
+ }
365
+ // Render current view
366
+ async function render() {
367
+ if (state.view === 'status') {
368
+ stopLogsAutoRefresh();
369
+ renderStatus();
370
+ }
371
+ else if (state.view === 'config') {
372
+ stopLogsAutoRefresh();
373
+ renderConfig();
374
+ }
375
+ else if (state.view === 'logs') {
376
+ await renderLogs();
377
+ startLogsAutoRefresh();
378
+ }
379
+ }
380
+ // Create a centered modal box
381
+ // Helper to create modal element (for custom modals with special logic)
382
+ function createModal(title, height = 'shrink') {
383
+ return blessed_1.default.box({
384
+ parent: screen,
385
+ top: 'center',
386
+ left: 'center',
387
+ width: '60%',
388
+ height,
389
+ border: { type: 'line' },
390
+ style: {
391
+ border: { fg: 'cyan' },
392
+ fg: 'white',
393
+ },
394
+ tags: true,
395
+ label: ` ${title} `,
396
+ });
397
+ }
398
+ // Helper to create semi-transparent overlay
399
+ function createOverlay() {
400
+ return blessed_1.default.box({
401
+ parent: screen,
402
+ top: 0,
403
+ left: 0,
404
+ width: '100%',
405
+ height: '100%',
406
+ style: {
407
+ bg: 'black',
408
+ transparent: true,
409
+ },
410
+ });
411
+ }
412
+ // Modal helper functions now use ModalController
413
+ /**
414
+ * Show error modal
415
+ * @param message - Error message to display
416
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
417
+ * Pass empty function for complex flows where outer function manages handlers.
418
+ */
419
+ async function showError(message, onClose) {
420
+ await modalController.showError(message, onClose || (() => {
421
+ registerHandlers();
422
+ render();
423
+ }));
424
+ }
425
+ /**
426
+ * Show success modal
427
+ * @param message - Success message to display
428
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
429
+ * Pass empty function for complex flows where outer function manages handlers.
430
+ */
431
+ async function showSuccess(message, onClose) {
432
+ await modalController.showSuccess(message, onClose || (() => {
433
+ registerHandlers();
434
+ render();
435
+ }));
436
+ }
437
+ // Number input modal
438
+ async function editNumber(field) {
439
+ unregisterHandlers();
440
+ return new Promise((resolve) => {
441
+ const handleClose = () => {
442
+ registerHandlers();
443
+ render();
444
+ };
445
+ const overlay = createOverlay();
446
+ const modal = createModal(`Edit ${field.label}`, 10);
447
+ const infoText = blessed_1.default.text({
448
+ parent: modal,
449
+ top: 1,
450
+ left: 2,
451
+ content: `Current: ${field.value}`,
452
+ tags: true,
453
+ });
454
+ const inputBox = blessed_1.default.textbox({
455
+ parent: modal,
456
+ top: 3,
457
+ left: 2,
458
+ right: 2,
459
+ height: 3,
460
+ inputOnFocus: true,
461
+ border: { type: 'line' },
462
+ style: {
463
+ border: { fg: 'white' },
464
+ focus: { border: { fg: 'green' } },
465
+ },
466
+ });
467
+ blessed_1.default.text({
468
+ parent: modal,
469
+ bottom: 1,
470
+ left: 2,
471
+ content: '{gray-fg}[Enter] Confirm [ESC] Cancel{/gray-fg}',
472
+ tags: true,
473
+ });
474
+ inputBox.setValue(String(field.value));
475
+ screen.append(overlay);
476
+ screen.append(modal);
477
+ screen.render();
478
+ inputBox.focus();
479
+ inputBox.on('submit', async (value) => {
480
+ const numValue = parseInt(value, 10);
481
+ if (!isNaN(numValue)) {
482
+ if (field.validation) {
483
+ const error = field.validation(numValue);
484
+ if (error) {
485
+ infoText.setContent(`{red-fg}Error: ${error}{/red-fg}`);
486
+ screen.render();
487
+ inputBox.focus();
488
+ return;
489
+ }
490
+ }
491
+ field.value = numValue;
492
+ updateHasChanges();
493
+ }
494
+ screen.remove(modal);
495
+ screen.remove(overlay);
496
+ handleClose();
497
+ resolve();
498
+ });
499
+ inputBox.on('cancel', () => {
500
+ screen.remove(modal);
501
+ screen.remove(overlay);
502
+ handleClose();
503
+ resolve();
504
+ });
505
+ });
506
+ }
507
+ // Toggle/select modal
508
+ async function editSelect(field) {
509
+ unregisterHandlers();
510
+ return new Promise((resolve) => {
511
+ const handleClose = () => {
512
+ registerHandlers();
513
+ render();
514
+ };
515
+ const options = field.options || [];
516
+ let selectedOption = field.type === 'toggle'
517
+ ? (field.value ? 1 : 0)
518
+ : options.indexOf(String(field.value));
519
+ if (selectedOption < 0)
520
+ selectedOption = 0;
521
+ const overlay = createOverlay();
522
+ const modal = createModal(field.label, options.length + 6);
523
+ function renderOptions() {
524
+ let content = '\n';
525
+ for (let i = 0; i < options.length; i++) {
526
+ const isSelected = i === selectedOption;
527
+ const indicator = isSelected ? '●' : '○';
528
+ if (isSelected) {
529
+ content += ` {cyan-fg}${indicator} ${options[i]}{/cyan-fg}\n`;
530
+ }
531
+ else {
532
+ content += ` {gray-fg}${indicator} ${options[i]}{/gray-fg}\n`;
533
+ }
534
+ }
535
+ // Add warning for 0.0.0.0
536
+ if (field.key === 'host' && options[selectedOption] === '0.0.0.0') {
537
+ content += '\n {yellow-fg}⚠ Warning: Exposes router to network{/yellow-fg}';
538
+ }
539
+ content += '\n\n{gray-fg} [↑/↓] Select [Enter] Confirm [ESC] Cancel{/gray-fg}';
540
+ modal.setContent(content);
541
+ screen.render();
542
+ }
543
+ screen.append(overlay);
544
+ screen.append(modal);
545
+ renderOptions();
546
+ modal.focus();
547
+ modal.key(['up', 'k'], () => {
548
+ selectedOption = Math.max(0, selectedOption - 1);
549
+ renderOptions();
550
+ });
551
+ modal.key(['down', 'j'], () => {
552
+ selectedOption = Math.min(options.length - 1, selectedOption + 1);
553
+ renderOptions();
554
+ });
555
+ modal.key(['enter'], () => {
556
+ if (field.type === 'toggle') {
557
+ field.value = selectedOption === 1;
558
+ }
559
+ else {
560
+ field.value = options[selectedOption];
561
+ }
562
+ updateHasChanges();
563
+ screen.remove(modal);
564
+ screen.remove(overlay);
565
+ handleClose();
566
+ resolve();
567
+ });
568
+ modal.key(['escape'], () => {
569
+ screen.remove(modal);
570
+ screen.remove(overlay);
571
+ handleClose();
572
+ resolve();
573
+ });
574
+ });
575
+ }
576
+ /**
577
+ * Show unsaved changes dialog
578
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
579
+ * Pass empty function for complex flows where outer function manages handlers.
580
+ */
581
+ async function showUnsavedDialog(onClose) {
582
+ return modalController.showUnsavedDialog(onClose || (() => {
583
+ registerHandlers();
584
+ render();
585
+ }));
586
+ }
587
+ /**
588
+ * Show restart confirmation dialog
589
+ * @param onClose - Optional callback when modal closes. Defaults to registering handlers + render.
590
+ * Pass empty function for complex flows where outer function manages handlers.
591
+ */
592
+ async function showRestartDialog(onClose) {
593
+ return modalController.showRestartDialog('Router', onClose || (() => {
594
+ registerHandlers();
595
+ render();
596
+ }));
597
+ }
598
+ // Save changes
599
+ async function saveChanges() {
600
+ if (!state.config)
601
+ return;
602
+ // Unregister handlers before showing any modals
603
+ unregisterHandlers();
604
+ // Check if router is running and prompt for restart
605
+ const wasRunning = state.isRunning;
606
+ let shouldRestart = false;
607
+ if (wasRunning) {
608
+ // Pass empty onClose - saveChanges() manages handler registration for entire flow
609
+ shouldRestart = await showRestartDialog(() => { });
610
+ }
611
+ // Show progress
612
+ const progressModal = modalController.showProgress('Saving configuration...');
613
+ try {
614
+ // Build updates
615
+ const updates = {};
616
+ for (const field of state.fields) {
617
+ if (field.value !== field.originalValue) {
618
+ updates[field.key] = field.value;
619
+ }
620
+ }
621
+ if (Object.keys(updates).length > 0) {
622
+ await router_manager_js_1.routerManager.updateConfig(updates);
623
+ }
624
+ if (wasRunning && shouldRestart) {
625
+ progressModal.setContent('\n {cyan-fg}Restarting router...{/cyan-fg}');
626
+ screen.render();
627
+ await router_manager_js_1.routerManager.restart();
628
+ }
629
+ // Refresh state
630
+ await refreshStatus();
631
+ // Update original values
632
+ for (const field of state.fields) {
633
+ field.originalValue = field.value;
634
+ }
635
+ state.hasChanges = false;
636
+ modalController.closeProgress(progressModal);
637
+ // Pass empty onClose - saveChanges() manages handler registration for entire flow
638
+ await showSuccess('Configuration saved', () => { });
639
+ }
640
+ catch (err) {
641
+ modalController.closeProgress(progressModal);
642
+ // Pass empty onClose - saveChanges() manages handler registration for entire flow
643
+ await showError(err instanceof Error ? err.message : 'Unknown error', () => { });
644
+ }
645
+ // Re-register handlers after all modals are closed
646
+ registerHandlers();
647
+ render();
648
+ }
649
+ // Start router
650
+ async function startRouter() {
651
+ const progressModal = modalController.showProgress('Starting router...');
652
+ try {
653
+ await router_manager_js_1.routerManager.start();
654
+ await refreshStatus();
655
+ // Update modal to show success
656
+ progressModal.setContent('\n {green-fg}✓ Router started{/green-fg}');
657
+ screen.render();
658
+ await new Promise(resolve => setTimeout(resolve, 1000));
659
+ modalController.closeProgress(progressModal);
660
+ render();
661
+ }
662
+ catch (err) {
663
+ modalController.closeProgress(progressModal);
664
+ await showError(err instanceof Error ? err.message : 'Failed to start router');
665
+ }
666
+ }
667
+ // Stop router
668
+ async function stopRouter() {
669
+ const progressModal = modalController.showProgress('Stopping router...');
670
+ try {
671
+ await router_manager_js_1.routerManager.stop();
672
+ await refreshStatus();
673
+ // Update modal to show success
674
+ progressModal.setContent('\n {green-fg}✓ Router stopped{/green-fg}');
675
+ screen.render();
676
+ await new Promise(resolve => setTimeout(resolve, 1000));
677
+ modalController.closeProgress(progressModal);
678
+ render();
679
+ }
680
+ catch (err) {
681
+ modalController.closeProgress(progressModal);
682
+ await showError(err instanceof Error ? err.message : 'Failed to stop router');
683
+ }
684
+ }
685
+ // Restart router
686
+ async function restartRouter() {
687
+ const progressModal = modalController.showProgress('Restarting router...');
688
+ try {
689
+ await router_manager_js_1.routerManager.restart();
690
+ await refreshStatus();
691
+ // Update modal to show success
692
+ progressModal.setContent('\n {green-fg}✓ Router restarted{/green-fg}');
693
+ screen.render();
694
+ await new Promise(resolve => setTimeout(resolve, 1000));
695
+ modalController.closeProgress(progressModal);
696
+ render();
697
+ }
698
+ catch (err) {
699
+ modalController.closeProgress(progressModal);
700
+ await showError(err instanceof Error ? err.message : 'Failed to restart router');
701
+ }
702
+ }
703
+ // Toggle auto-refresh
704
+ function toggleAutoRefresh() {
705
+ if (state.logsRefreshInterval) {
706
+ // Currently on - turn it off
707
+ stopLogsAutoRefresh();
708
+ }
709
+ else {
710
+ // Currently off - turn it on
711
+ startLogsAutoRefresh();
712
+ }
713
+ renderLogs(); // Re-render to update status
714
+ }
715
+ // Handle edit action for selected field
716
+ async function handleEdit() {
717
+ if (state.view !== 'config')
718
+ return;
719
+ const field = state.fields[state.selectedIndex];
720
+ switch (field.type) {
721
+ case 'number':
722
+ await editNumber(field);
723
+ break;
724
+ case 'toggle':
725
+ case 'select':
726
+ await editSelect(field);
727
+ break;
728
+ }
729
+ }
730
+ // Handle escape/cancel
731
+ async function handleEscape() {
732
+ // Don't handle if modal is open
733
+ if (modalController.isModalOpen())
734
+ return;
735
+ if (state.view === 'logs') {
736
+ state.view = 'status';
737
+ await render();
738
+ return;
739
+ }
740
+ if (state.view === 'config') {
741
+ if (state.hasChanges) {
742
+ unregisterHandlers();
743
+ // Pass empty onClose - handleEscape manages handler registration
744
+ const result = await showUnsavedDialog(() => { });
745
+ if (result === 'save') {
746
+ await saveChanges();
747
+ state.view = 'status';
748
+ }
749
+ else if (result === 'discard') {
750
+ // Reset field values
751
+ for (const field of state.fields) {
752
+ field.value = field.originalValue;
753
+ }
754
+ state.hasChanges = false;
755
+ state.view = 'status';
756
+ }
757
+ // 'continue' - stay in config view
758
+ registerHandlers();
759
+ await render();
760
+ }
761
+ else {
762
+ state.view = 'status';
763
+ await render();
764
+ }
765
+ return;
766
+ }
767
+ // In status view, exit to main monitor
768
+ if (state.hasChanges) {
769
+ unregisterHandlers();
770
+ // Pass empty onClose - handleEscape manages handler registration
771
+ const result = await showUnsavedDialog(() => { });
772
+ registerHandlers();
773
+ if (result === 'save') {
774
+ await saveChanges();
775
+ cleanup();
776
+ onBack();
777
+ }
778
+ else if (result === 'discard') {
779
+ cleanup();
780
+ onBack();
781
+ }
782
+ // 'continue' - stay in status view
783
+ await render();
784
+ }
785
+ else {
786
+ cleanup();
787
+ onBack();
788
+ }
789
+ }
790
+ // Key handlers
791
+ const keyHandlers = {
792
+ up: () => {
793
+ if (modalController.isModalOpen())
794
+ return;
795
+ if (state.view === 'config') {
796
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
797
+ render();
798
+ }
799
+ },
800
+ down: () => {
801
+ if (modalController.isModalOpen())
802
+ return;
803
+ if (state.view === 'config') {
804
+ state.selectedIndex = Math.min(state.fields.length - 1, state.selectedIndex + 1);
805
+ render();
806
+ }
807
+ },
808
+ enter: () => {
809
+ if (modalController.isModalOpen())
810
+ return;
811
+ handleEdit();
812
+ },
813
+ config: () => {
814
+ if (modalController.isModalOpen())
815
+ return;
816
+ if (state.view === 'status') {
817
+ state.view = 'config';
818
+ state.selectedIndex = 0;
819
+ render();
820
+ }
821
+ },
822
+ startStopOrSave: () => {
823
+ if (modalController.isModalOpen())
824
+ return;
825
+ if (state.view === 'status' && !state.isRunning) {
826
+ startRouter();
827
+ }
828
+ else if (state.view === 'status' && state.isRunning) {
829
+ stopRouter();
830
+ }
831
+ else if (state.view === 'config' && state.hasChanges) {
832
+ saveChanges();
833
+ }
834
+ },
835
+ toggleLogType: () => {
836
+ if (modalController.isModalOpen())
837
+ return;
838
+ if (state.view === 'logs') {
839
+ state.logType = state.logType === 'stdout' ? 'stderr' : 'stdout';
840
+ render();
841
+ }
842
+ },
843
+ restartOrRefresh: () => {
844
+ if (modalController.isModalOpen())
845
+ return;
846
+ // Combined handler for 'r'/'R' key (different actions in different views)
847
+ if (state.view === 'status' && state.isRunning) {
848
+ restartRouter();
849
+ }
850
+ else if (state.view === 'logs') {
851
+ renderLogs();
852
+ }
853
+ },
854
+ logs: () => {
855
+ if (modalController.isModalOpen())
856
+ return;
857
+ if (state.view === 'status') {
858
+ state.view = 'logs';
859
+ render();
860
+ }
861
+ },
862
+ toggleRefresh: () => {
863
+ if (modalController.isModalOpen())
864
+ return;
865
+ if (state.view === 'logs') {
866
+ toggleAutoRefresh();
867
+ }
868
+ },
869
+ escape: () => {
870
+ handleEscape();
871
+ },
872
+ quit: () => {
873
+ if (modalController.isModalOpen())
874
+ return;
875
+ screen.destroy();
876
+ process.exit(0);
877
+ },
878
+ };
879
+ // Unregister handlers (for modal dialogs)
880
+ function unregisterHandlers() {
881
+ screen.unkey('up', keyHandlers.up);
882
+ screen.unkey('k', keyHandlers.up);
883
+ screen.unkey('down', keyHandlers.down);
884
+ screen.unkey('j', keyHandlers.down);
885
+ screen.unkey('enter', keyHandlers.enter);
886
+ screen.unkey('c', keyHandlers.config);
887
+ screen.unkey('C', keyHandlers.config);
888
+ screen.unkey('s', keyHandlers.startStopOrSave);
889
+ screen.unkey('S', keyHandlers.startStopOrSave);
890
+ screen.unkey('t', keyHandlers.toggleLogType);
891
+ screen.unkey('T', keyHandlers.toggleLogType);
892
+ screen.unkey('r', keyHandlers.restartOrRefresh);
893
+ screen.unkey('R', keyHandlers.restartOrRefresh);
894
+ screen.unkey('l', keyHandlers.logs);
895
+ screen.unkey('L', keyHandlers.logs);
896
+ screen.unkey('f', keyHandlers.toggleRefresh);
897
+ screen.unkey('F', keyHandlers.toggleRefresh);
898
+ screen.unkey('escape', keyHandlers.escape);
899
+ screen.unkey('q', keyHandlers.quit);
900
+ screen.unkey('Q', keyHandlers.quit);
901
+ }
902
+ // Register handlers (all handlers registered unconditionally)
903
+ function registerHandlers() {
904
+ // Register all handlers - they contain view guards internally
905
+ screen.key(['up', 'k'], keyHandlers.up);
906
+ screen.key(['down', 'j'], keyHandlers.down);
907
+ screen.key(['enter'], keyHandlers.enter);
908
+ screen.key(['s', 'S'], keyHandlers.startStopOrSave);
909
+ screen.key(['t', 'T'], keyHandlers.toggleLogType);
910
+ screen.key(['r', 'R'], keyHandlers.restartOrRefresh);
911
+ screen.key(['c', 'C'], keyHandlers.config);
912
+ screen.key(['l', 'L'], keyHandlers.logs);
913
+ screen.key(['f', 'F'], keyHandlers.toggleRefresh);
914
+ screen.key(['escape'], keyHandlers.escape);
915
+ screen.key(['q', 'Q'], keyHandlers.quit);
916
+ }
917
+ // Cleanup function (for exiting router screen)
918
+ function cleanup() {
919
+ stopLogsAutoRefresh();
920
+ unregisterHandlers();
921
+ screen.remove(contentBox);
922
+ }
923
+ // Initial registration
924
+ registerHandlers();
925
+ // Initial render
926
+ render();
927
+ }
928
+ //# sourceMappingURL=RouterApp.js.map