@appkit/llamacpp-cli 2.0.0 → 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.
- package/README.md +271 -277
- package/dist/cli.js +133 -23
- package/dist/cli.js.map +1 -1
- package/dist/commands/admin/config.d.ts +1 -1
- package/dist/commands/admin/config.js +5 -5
- package/dist/commands/admin/config.js.map +1 -1
- package/dist/commands/admin/log-config.d.ts +11 -0
- package/dist/commands/admin/log-config.d.ts.map +1 -0
- package/dist/commands/admin/log-config.js +159 -0
- package/dist/commands/admin/log-config.js.map +1 -0
- package/dist/commands/admin/logs.d.ts +2 -3
- package/dist/commands/admin/logs.d.ts.map +1 -1
- package/dist/commands/admin/logs.js +6 -48
- package/dist/commands/admin/logs.js.map +1 -1
- package/dist/commands/admin/status.d.ts.map +1 -1
- package/dist/commands/admin/status.js +1 -0
- package/dist/commands/admin/status.js.map +1 -1
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -196
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create.d.ts +3 -2
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +24 -97
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/delete.d.ts.map +1 -1
- package/dist/commands/delete.js +7 -24
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/internal/server-wrapper.d.ts +15 -0
- package/dist/commands/internal/server-wrapper.d.ts.map +1 -0
- package/dist/commands/internal/server-wrapper.js +126 -0
- package/dist/commands/internal/server-wrapper.js.map +1 -0
- package/dist/commands/logs-all.d.ts +0 -2
- package/dist/commands/logs-all.d.ts.map +1 -1
- package/dist/commands/logs-all.js +1 -61
- package/dist/commands/logs-all.js.map +1 -1
- package/dist/commands/logs.d.ts +2 -5
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +104 -120
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/migrate-labels.d.ts +12 -0
- package/dist/commands/migrate-labels.d.ts.map +1 -0
- package/dist/commands/migrate-labels.js +160 -0
- package/dist/commands/migrate-labels.js.map +1 -0
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +2 -1
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +22 -48
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/router/config.d.ts +1 -1
- package/dist/commands/router/config.js +6 -6
- package/dist/commands/router/config.js.map +1 -1
- package/dist/commands/router/logs.d.ts +2 -4
- package/dist/commands/router/logs.d.ts.map +1 -1
- package/dist/commands/router/logs.js +34 -189
- package/dist/commands/router/logs.js.map +1 -1
- package/dist/commands/router/status.d.ts.map +1 -1
- package/dist/commands/router/status.js +1 -0
- package/dist/commands/router/status.js.map +1 -1
- package/dist/commands/server-show.d.ts.map +1 -1
- package/dist/commands/server-show.js +3 -0
- package/dist/commands/server-show.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +21 -72
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +10 -26
- package/dist/commands/stop.js.map +1 -1
- package/dist/launchers/llamacpp-admin +8 -0
- package/dist/launchers/llamacpp-router +8 -0
- package/dist/launchers/llamacpp-server +8 -0
- package/dist/lib/admin-manager.d.ts +4 -0
- package/dist/lib/admin-manager.d.ts.map +1 -1
- package/dist/lib/admin-manager.js +42 -18
- package/dist/lib/admin-manager.js.map +1 -1
- package/dist/lib/admin-server.d.ts +48 -1
- package/dist/lib/admin-server.d.ts.map +1 -1
- package/dist/lib/admin-server.js +632 -238
- package/dist/lib/admin-server.js.map +1 -1
- package/dist/lib/config-generator.d.ts +1 -0
- package/dist/lib/config-generator.d.ts.map +1 -1
- package/dist/lib/config-generator.js +12 -5
- package/dist/lib/config-generator.js.map +1 -1
- package/dist/lib/keyboard-manager.d.ts +162 -0
- package/dist/lib/keyboard-manager.d.ts.map +1 -0
- package/dist/lib/keyboard-manager.js +247 -0
- package/dist/lib/keyboard-manager.js.map +1 -0
- package/dist/lib/label-migration.d.ts +65 -0
- package/dist/lib/label-migration.d.ts.map +1 -0
- package/dist/lib/label-migration.js +458 -0
- package/dist/lib/label-migration.js.map +1 -0
- package/dist/lib/launchctl-manager.d.ts +9 -0
- package/dist/lib/launchctl-manager.d.ts.map +1 -1
- package/dist/lib/launchctl-manager.js +65 -19
- package/dist/lib/launchctl-manager.js.map +1 -1
- package/dist/lib/log-management-service.d.ts +51 -0
- package/dist/lib/log-management-service.d.ts.map +1 -0
- package/dist/lib/log-management-service.js +124 -0
- package/dist/lib/log-management-service.js.map +1 -0
- package/dist/lib/log-workers.d.ts +70 -0
- package/dist/lib/log-workers.d.ts.map +1 -0
- package/dist/lib/log-workers.js +217 -0
- package/dist/lib/log-workers.js.map +1 -0
- package/dist/lib/model-downloader.d.ts +9 -1
- package/dist/lib/model-downloader.d.ts.map +1 -1
- package/dist/lib/model-downloader.js +98 -1
- package/dist/lib/model-downloader.js.map +1 -1
- package/dist/lib/model-management-service.d.ts +60 -0
- package/dist/lib/model-management-service.d.ts.map +1 -0
- package/dist/lib/model-management-service.js +246 -0
- package/dist/lib/model-management-service.js.map +1 -0
- package/dist/lib/model-management-service.test.d.ts +2 -0
- package/dist/lib/model-management-service.test.d.ts.map +1 -0
- package/dist/lib/model-management-service.test.js.map +1 -0
- package/dist/lib/model-scanner.d.ts +15 -3
- package/dist/lib/model-scanner.d.ts.map +1 -1
- package/dist/lib/model-scanner.js +174 -17
- package/dist/lib/model-scanner.js.map +1 -1
- package/dist/lib/openapi-spec.d.ts +1335 -0
- package/dist/lib/openapi-spec.d.ts.map +1 -0
- package/dist/lib/openapi-spec.js +1017 -0
- package/dist/lib/openapi-spec.js.map +1 -0
- package/dist/lib/router-logger.d.ts +1 -1
- package/dist/lib/router-logger.d.ts.map +1 -1
- package/dist/lib/router-logger.js +13 -11
- package/dist/lib/router-logger.js.map +1 -1
- package/dist/lib/router-manager.d.ts +4 -0
- package/dist/lib/router-manager.d.ts.map +1 -1
- package/dist/lib/router-manager.js +30 -18
- package/dist/lib/router-manager.js.map +1 -1
- package/dist/lib/router-server.d.ts.map +1 -1
- package/dist/lib/router-server.js +22 -12
- package/dist/lib/router-server.js.map +1 -1
- package/dist/lib/server-config-service.d.ts +51 -0
- package/dist/lib/server-config-service.d.ts.map +1 -0
- package/dist/lib/server-config-service.js +310 -0
- package/dist/lib/server-config-service.js.map +1 -0
- package/dist/lib/server-config-service.test.d.ts +2 -0
- package/dist/lib/server-config-service.test.d.ts.map +1 -0
- package/dist/lib/server-config-service.test.js.map +1 -0
- package/dist/lib/server-lifecycle-service.d.ts +172 -0
- package/dist/lib/server-lifecycle-service.d.ts.map +1 -0
- package/dist/lib/server-lifecycle-service.js +619 -0
- package/dist/lib/server-lifecycle-service.js.map +1 -0
- package/dist/lib/state-manager.d.ts +18 -1
- package/dist/lib/state-manager.d.ts.map +1 -1
- package/dist/lib/state-manager.js +51 -2
- package/dist/lib/state-manager.js.map +1 -1
- package/dist/lib/status-checker.d.ts +11 -4
- package/dist/lib/status-checker.d.ts.map +1 -1
- package/dist/lib/status-checker.js +34 -1
- package/dist/lib/status-checker.js.map +1 -1
- package/dist/lib/validation-service.d.ts +43 -0
- package/dist/lib/validation-service.d.ts.map +1 -0
- package/dist/lib/validation-service.js +112 -0
- package/dist/lib/validation-service.js.map +1 -0
- package/dist/lib/validation-service.test.d.ts +2 -0
- package/dist/lib/validation-service.test.d.ts.map +1 -0
- package/dist/lib/validation-service.test.js.map +1 -0
- package/dist/scripts/http-log-filter.sh +8 -0
- package/dist/tui/ConfigApp.d.ts.map +1 -1
- package/dist/tui/ConfigApp.js +222 -184
- package/dist/tui/ConfigApp.js.map +1 -1
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -1
- package/dist/tui/HistoricalMonitorApp.js +12 -0
- package/dist/tui/HistoricalMonitorApp.js.map +1 -1
- package/dist/tui/ModelsApp.d.ts.map +1 -1
- package/dist/tui/ModelsApp.js +93 -17
- package/dist/tui/ModelsApp.js.map +1 -1
- package/dist/tui/MonitorApp.d.ts.map +1 -1
- package/dist/tui/MonitorApp.js +1 -3
- package/dist/tui/MonitorApp.js.map +1 -1
- package/dist/tui/MultiServerMonitorApp.d.ts +3 -3
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +724 -508
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/tui/RootNavigator.d.ts.map +1 -1
- package/dist/tui/RootNavigator.js +17 -1
- package/dist/tui/RootNavigator.js.map +1 -1
- package/dist/tui/RouterApp.d.ts +6 -0
- package/dist/tui/RouterApp.d.ts.map +1 -0
- package/dist/tui/RouterApp.js +928 -0
- package/dist/tui/RouterApp.js.map +1 -0
- package/dist/tui/SearchApp.d.ts.map +1 -1
- package/dist/tui/SearchApp.js +27 -6
- package/dist/tui/SearchApp.js.map +1 -1
- package/dist/tui/shared/modal-controller.d.ts +65 -0
- package/dist/tui/shared/modal-controller.d.ts.map +1 -0
- package/dist/tui/shared/modal-controller.js +625 -0
- package/dist/tui/shared/modal-controller.js.map +1 -0
- package/dist/tui/shared/overlay-utils.d.ts +7 -0
- package/dist/tui/shared/overlay-utils.d.ts.map +1 -0
- package/dist/tui/shared/overlay-utils.js +54 -0
- package/dist/tui/shared/overlay-utils.js.map +1 -0
- package/dist/types/admin-config.d.ts +15 -2
- package/dist/types/admin-config.d.ts.map +1 -1
- package/dist/types/model-info.d.ts +5 -0
- package/dist/types/model-info.d.ts.map +1 -1
- package/dist/types/router-config.d.ts +2 -2
- package/dist/types/router-config.d.ts.map +1 -1
- package/dist/types/server-config.d.ts +8 -0
- package/dist/types/server-config.d.ts.map +1 -1
- package/dist/types/server-config.js +25 -0
- package/dist/types/server-config.js.map +1 -1
- package/dist/utils/http-log-filter.d.ts +10 -0
- package/dist/utils/http-log-filter.d.ts.map +1 -0
- package/dist/utils/http-log-filter.js +84 -0
- package/dist/utils/http-log-filter.js.map +1 -0
- package/dist/utils/log-parser.d.ts.map +1 -1
- package/dist/utils/log-parser.js +7 -4
- package/dist/utils/log-parser.js.map +1 -1
- package/dist/utils/log-utils.d.ts +59 -4
- package/dist/utils/log-utils.d.ts.map +1 -1
- package/dist/utils/log-utils.js +150 -11
- package/dist/utils/log-utils.js.map +1 -1
- package/dist/utils/shard-utils.d.ts +72 -0
- package/dist/utils/shard-utils.d.ts.map +1 -0
- package/dist/utils/shard-utils.js +168 -0
- package/dist/utils/shard-utils.js.map +1 -0
- package/package.json +18 -4
- package/src/launchers/llamacpp-admin +8 -0
- package/src/launchers/llamacpp-router +8 -0
- package/src/launchers/llamacpp-server +8 -0
- package/web/dist/assets/index-Byhoy86V.css +1 -0
- package/web/dist/assets/index-HSrgvray.js +50 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Bin89Lwr.css +0 -1
- 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
|